#nullable enable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net;
using System.Threading;
using UnityEngine;
using IGP.UnitySDK.Core;
using IGP.UnitySDK.Models;
using IGP.UnitySDK.Protocol;
using Newtonsoft.Json.Linq;

namespace IGP.UnitySDK.Network
{
    public class IGPNetwork : IIGPNetwork
    {
        private readonly IGPRuntimeManager _manager;
        private readonly object _lock = new object();

        // Sessions: Key = Remote Player ID
        private enum SessionStatus { None, Requested, Connected }
        private readonly Dictionary<string, SessionStatus> _sessions = new Dictionary<string, SessionStatus>();
        
        // Remote Player RTT storage (Value = Server RTT in seconds)
        private readonly Dictionary<string, float> _remotePlayerServerRtt = new Dictionary<string, float>();
        private float _lastRttBroadcastTime = 0f;
        private const float RttBroadcastInterval = 2.0f; // Seconds
        private const uint RttMessageType = 1000; // Special type for RTT exchange
        private IGPReliableTransportOptions _transportOptions = IGPReliableTransportOptions.Default;
        private IGPReliableMessageReassembler _reassembler;
        
        // Incoming Data Queue
        private struct IncomingPacket
        {
            public IGPPlayerID LocalPeer;
            public IGPGamePeer RemotePeer;
            public byte[] Data;
            public uint MessageType;
            public long Timestamp;
        }
        
        private readonly ConcurrentQueue<IncomingPacket> _packetQueue = new ConcurrentQueue<IncomingPacket>();
        
        // Blocked Message Types
        private readonly HashSet<uint> _blockedTypes = new HashSet<uint>();

        // Events (Unity Style)
        public readonly IGPSessionRequestEvent OnCreateSessionRequest = new IGPSessionRequestEvent();
        public readonly IGPSessionFailedEvent OnCreateSessionFailed = new IGPSessionFailedEvent();
        public readonly IGPRawSessionRequestEvent OnNetworkCreateRawSessionRequest = new IGPRawSessionRequestEvent();
        public readonly IGPRawSessionFailedEvent OnNetworkCreateRawSessionFailed = new IGPRawSessionFailedEvent();
        public readonly IGPDataReceivedEvent OnDataReceived = new IGPDataReceivedEvent();

        public IGPNetwork(IGPRuntimeManager manager)
        {
            _manager = manager;
            _reassembler = new IGPReliableMessageReassembler(_transportOptions);
        }

        internal void ApplyTransportOptions(IGPReliableTransportOptions options)
        {
            if (options == null)
            {
                throw new ArgumentNullException(nameof(options));
            }

            lock (_lock)
            {
                _transportOptions = options;
                _reassembler.Reset();
                _reassembler = new IGPReliableMessageReassembler(_transportOptions);
            }
        }

        public void Update()
        {
            // Periodically broadcast local RTT to other players
            if (Time.time - _lastRttBroadcastTime > RttBroadcastInterval)
            {
                var stats = _manager.KcpRTTStats;
                if (stats != null && stats.SampleCount > 0)
                {
                    float avgRtt = stats.AvgRTT;
                    byte[] rttData = BitConverter.GetBytes(avgRtt);
                    // Broadcast to everyone in room (targetId = "")
                    SendData(new IGPPlayerID(_manager.PlayerId), new IGPPlayerID(""), rttData, (uint)rttData.Length, RttMessageType);
                }
                _lastRttBroadcastTime = Time.time;
            }

            CleanupExpiredReliableMessages();
        }

        public void Dispose()
        {
            _reassembler.Reset();
        }

        #region Internal Message Handling

        /// <summary>
        /// Handles incoming P2P data messages pushed from IGPRuntimeManager.
        /// </summary>
        public void HandleIncomingP2P(object? content)
        {
            if (content == null) return;

            try
            {
                var p2pMsg = ConvertToP2PMessagePayload(content);

                if (p2pMsg == null) return;

                CleanupExpiredReliableMessages();

                uint effectiveMessageType = p2pMsg.reliableMessageType ?? p2pMsg.messageType;

                // Handle RTT Exchange messages (Special internal type)
                if (!IsReliableChunk(p2pMsg) && p2pMsg.messageType == RttMessageType && p2pMsg.data != null)
                {
                    try
                    {
                        var dataBytes = Convert.FromBase64String(p2pMsg.data);
                        if (dataBytes.Length >= 4)
                        {
                            float remoteServerRtt = BitConverter.ToSingle(dataBytes, 0);
                            lock (_lock)
                            {
                                _remotePlayerServerRtt[p2pMsg.senderId] = remoteServerRtt;
                            }
                        }
                    }
                    catch { /* ignore malformed rtt */ }
                    return;
                }

                // Check Blocked
                if (_blockedTypes.Contains(effectiveMessageType)) return;

                var remoteId = p2pMsg.senderId;
                var localId = _manager.PlayerId;

                lock (_lock)
                {
                    if (!_sessions.ContainsKey(remoteId))
                    {
                        // New Session Request
                        _sessions[remoteId] = SessionStatus.Requested;
                        
                        // Fire Event
                        var request = new CreateSessionRequest
                        {
                            local_peer = new IGPPlayerID(localId),
                            remote_peer = new IGPPlayerID(remoteId),
                            user_data = ""
                        };
                        
                        OnCreateSessionRequest.Invoke(request);
                    }
                    
                    if (_sessions[remoteId] == SessionStatus.Connected || _sessions[remoteId] == SessionStatus.Requested)
                    {
                        byte[]? dataBytes = TryDecodeIncomingPayload(remoteId, p2pMsg, out var resolvedMessageType);
                        if (dataBytes != null && dataBytes.Length > 0)
                        {
                            var packet = new IncomingPacket
                            {
                                LocalPeer = new IGPPlayerID(localId),
                                RemotePeer = new IGPGamePeer(new IGPPlayerID(remoteId), new IGPGameID(_manager.CurrentRoomId)), // Assume same game for p2p_data
                                Data = dataBytes,
                                MessageType = resolvedMessageType,
                                Timestamp = DateTime.Now.Ticks
                            };
                            _packetQueue.Enqueue(packet);

                            // Trigger Data Received Event
                            OnDataReceived.Invoke(new IGPDataReceived
                            {
                                remote_peer = new IGPPlayerID(remoteId),
                                data = dataBytes,
                                message_type = resolvedMessageType
                            });
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.LogError($"[IGPNetwork] Error parsing p2p_data: {ex.Message}");
            }
        }

        #endregion

        #region IIGPNetwork Implementation

        public IGPNetworkResult AcceptSessionRequest(IGPPlayerID local_peer, IGPPlayerID remote_peer)
        {
            lock (_lock)
            {
                if (_sessions.ContainsKey(remote_peer.id))
                {
                    _sessions[remote_peer.id] = SessionStatus.Connected;
                    return IGPNetworkResult.kSuccess;
                }
            }
            return IGPNetworkResult.kErrorInvalidState; // Or kErrorUnknown
        }

        public IGPNetworkResult CloseSession(IGPPlayerID local_peer, IGPPlayerID remote_peer)
        {
            lock (_lock)
            {
                if (_sessions.ContainsKey(remote_peer.id))
                {
                    _sessions.Remove(remote_peer.id);
                    // Notify remote? 
                    // Usually CloseSession just closes local. Remote detects via events.
                    // For now, we just remove local state.
                    return IGPNetworkResult.kSuccess;
                }
            }
            return IGPNetworkResult.kErrorUnknown;
        }

        public IGPNetworkResult GetSessionState(IGPPlayerID remote_peer, out IGPNetworkSessionState session_state)
        {
            session_state = new IGPNetworkSessionState();
            session_state.remote_peer = remote_peer;
            
            lock (_lock)
            {
                if (_sessions.TryGetValue(remote_peer.id, out var status))
                {
                    session_state.is_connected = (status == SessionStatus.Connected);
                    session_state.is_writable = session_state.is_connected;
                    return IGPNetworkResult.kSuccess;
                }
            }
            return IGPNetworkResult.kErrorUnknown;
        }

        public IGPNetworkResult SendData(IGPPlayerID local_peer, IGPPlayerID remote_peer, byte[] data_buf, uint data_len, uint message_type = 0)
        {
            // If remote_peer.id is empty, it means broadcast to room
            return InternalSendData(local_peer, remote_peer.id, data_buf, data_len, message_type, allowFragmentation: false);
        }

        public IGPNetworkResult SendReliableData(IGPPlayerID local_peer, IGPPlayerID remote_peer, byte[] data_buf, uint data_len, uint message_type = 0)
        {
            // If remote_peer.id is empty, it means broadcast to room
            return InternalSendData(local_peer, remote_peer.id, data_buf, data_len, message_type, allowFragmentation: true);
        }

        private IGPNetworkResult InternalSendData(IGPPlayerID local_peer, string targetId, byte[] data_buf, uint data_len, uint message_type, bool allowFragmentation)
        {
            if (data_buf == null || data_len == 0) return IGPNetworkResult.kErrorInvalidParam;
            if (data_len > int.MaxValue) return IGPNetworkResult.kErrorInvalidParam;
            int dataLength = (int)data_len;
            if (dataLength > data_buf.Length) return IGPNetworkResult.kErrorInvalidParam;

            if (allowFragmentation)
            {
                if (dataLength > _transportOptions.ReliableMessageMaxBytes)
                {
                    Debug.LogWarning($"[IGPNetwork] KCP payload length {data_len} exceeds reliable limit of {_transportOptions.ReliableMessageMaxBytes} bytes.");
                    return IGPNetworkResult.kErrorInvalidParam;
                }
            }
            else if (dataLength > _transportOptions.KcpDataPlanePayloadMaxBytes)
            {
                Debug.LogWarning($"[IGPNetwork] KCP direct payload length {data_len} exceeds limit of {_transportOptions.KcpDataPlanePayloadMaxBytes} bytes.");
                return IGPNetworkResult.kErrorInvalidParam;
            }

            // Check session for point-to-point (non-broadcast)
            if (!string.IsNullOrEmpty(targetId))
            {
                lock (_lock)
                {
                    // Auto-create session on send if not exists (Initiator)
                    if (!_sessions.ContainsKey(targetId))
                    {
                        _sessions[targetId] = SessionStatus.Connected; // Initiator is implicitly connected
                    }
                }
            }

            try
            {
                foreach (var payload in CreateOutgoingPayloads(local_peer.id, targetId, data_buf, dataLength, message_type, allowFragmentation))
                {
                    var msg = new Message
                    {
                        type = "p2p_data",
                        roomId = _manager.CurrentRoomId,
                        playerId = local_peer.id,
                        targetPlayerId = targetId,
                        content = payload,
                        reliable = true
                    };

                    _manager.SendP2PMessage(msg);
                }
                return IGPNetworkResult.kSuccess;
            }
            catch
            {
                return IGPNetworkResult.kErrorNetworkError;
            }
        }

        public bool IsDataReady(out IGPPlayerID local_peer, out uint data_len, out uint message_type)
        {
            if (_packetQueue.TryPeek(out var packet))
            {
                local_peer = packet.LocalPeer;
                data_len = (uint)packet.Data.Length;
                message_type = packet.MessageType;
                return true;
            }

            local_peer = default;
            data_len = 0;
            message_type = 0;
            return false;
        }

        public IGPNetworkResult ReadData(IGPPlayerID local_peer, out IGPPlayerID remote_peer, byte[] data_buf, uint data_len, uint message_type = 0)
        {
            remote_peer = default;
            
            if (_packetQueue.TryDequeue(out var packet))
            {
                remote_peer = packet.RemotePeer.player_id;
                
                if (data_buf.Length < packet.Data.Length)
                {
                    // Buffer too small, data loss or partial read?
                    // We copy what fits.
                    Array.Copy(packet.Data, data_buf, data_buf.Length);
                    return IGPNetworkResult.kErrorInvalidParam; // Or partial success?
                }
                
                Array.Copy(packet.Data, data_buf, packet.Data.Length);
                return IGPNetworkResult.kSuccess;
            }
            
            return IGPNetworkResult.kErrorUnknown;
        }

        public IGPNetworkResult SendRawData(IGPPlayerID local_peer, IGPGamePeer remote_game_peer, byte[] data_buf, uint data_len, bool reliable, uint message_type = 0)
        {
            // For now, only support same game (Room)
            // If we had GameID check: if (remote_game_peer.game_id != CurrentGameID) return kErrorServiceNotAvailable;

            _ = reliable;
            return InternalSendData(local_peer, remote_game_peer.player_id, data_buf, data_len, message_type, allowFragmentation: true);
        }

        public IGPNetworkResult AcceptRawSessionRequest(IGPPlayerID local_peer, IGPGamePeer remote_game_peer)
        {
            return AcceptSessionRequest(local_peer, remote_game_peer.player_id);
        }

        public IGPNetworkResult ReadRawData(IGPPlayerID local_peer, out IGPGamePeer remote_game_peer, byte[] data_buf, uint data_len, uint message_type = 0)
        {
            if (_packetQueue.TryDequeue(out var packet))
            {
                remote_game_peer = packet.RemotePeer;
                
                if (data_buf.Length < packet.Data.Length)
                {
                    Array.Copy(packet.Data, data_buf, data_buf.Length);
                    return IGPNetworkResult.kErrorInvalidParam;
                }
                
                Array.Copy(packet.Data, data_buf, packet.Data.Length);
                return IGPNetworkResult.kSuccess;
            }

            remote_game_peer = default;
            return IGPNetworkResult.kErrorUnknown;
        }

        public IGPNetworkResult ResolveHostname(string domain, out List<string> ip_list)
        {
            ip_list = new List<string>();
            try
            {
                var ips = Dns.GetHostAddresses(domain);
                foreach (var ip in ips)
                {
                    if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
                    {
                        ip_list.Add(ip.ToString());
                    }
                }
                return IGPNetworkResult.kSuccess;
            }
            catch
            {
                return IGPNetworkResult.kErrorUnknown;
            }
        }

        public IGPNetworkResult BlockMessageType(IGPPlayerID local_peer, uint message_type)
        {
            lock (_lock)
            {
                _blockedTypes.Add(message_type);
            }
            return IGPNetworkResult.kSuccess;
        }

        public IGPNetworkResult UnblockMessageType(IGPPlayerID local_peer, uint message_type)
        {
            lock (_lock)
            {
                _blockedTypes.Remove(message_type);
            }
            return IGPNetworkResult.kSuccess;
        }

        public uint GetPeerLatency(IGPPlayerID remote_peer)
        {
            float myRtt = _manager.KcpRTTStats?.AvgRTT ?? 0f;
            float hisRtt = 0f;
            
            lock (_lock)
            {
                _remotePlayerServerRtt.TryGetValue(remote_peer.id, out hisRtt);
            }

            // Estimate E2E latency as sum of both RTTs
            // If hisRtt is 0, we fallback to just my RTT (better than nothing)
            return (uint)((myRtt + hisRtt) * 1000);
        }

        /// <summary>
        /// Gets list of all player IDs we have active sessions or RTT data for.
        /// </summary>
        public List<string> GetConnectedPeerIds()
        {
            var peers = new HashSet<string>();
            lock (_lock)
            {
                foreach (var id in _sessions.Keys) peers.Add(id);
                foreach (var id in _remotePlayerServerRtt.Keys) peers.Add(id);
            }
            return new List<string>(peers);
        }

        /// <summary>
        /// Resets all session states and clears packet queues.
        /// </summary>
        public void ResetSessionState()
        {
            lock (_lock)
            {
                _sessions.Clear();
                _remotePlayerServerRtt.Clear();
                _reassembler.Reset();
                while (_packetQueue.TryDequeue(out _)) { }
            }
        }

        #endregion

        private IEnumerable<P2PMessagePayload> CreateOutgoingPayloads(
            string senderId,
            string targetId,
            byte[] dataBuffer,
            int dataLength,
            uint messageType,
            bool allowFragmentation)
        {
            var payloadBytes = new byte[dataLength];
            Buffer.BlockCopy(dataBuffer, 0, payloadBytes, 0, dataLength);

            if (!allowFragmentation || dataLength <= _transportOptions.KcpDataPlanePayloadMaxBytes)
            {
                yield return CreateStandardPayload(senderId, targetId, payloadBytes, messageType);
                yield break;
            }

            string messageId = Guid.NewGuid().ToString("N");
            var chunks = IGPReliableMessageFragmenter.Fragment(
                payloadBytes,
                messageType,
                targetId,
                _transportOptions,
                messageId);

            foreach (var chunk in chunks)
            {
                yield return new P2PMessagePayload
                {
                    senderId = senderId,
                    targetId = targetId,
                    data = Convert.ToBase64String(chunk.Payload),
                    messageType = messageType,
                    reliable = true,
                    reliableMessageId = chunk.MessageId,
                    reliableChunkIndex = chunk.ChunkIndex,
                    reliableChunkCount = chunk.ChunkCount,
                    reliableTotalBytes = chunk.TotalBytes,
                    reliableMessageType = chunk.MessageType
                };
            }
        }

        private static P2PMessagePayload CreateStandardPayload(
            string senderId,
            string targetId,
            byte[] payloadBytes,
            uint messageType)
        {
            return new P2PMessagePayload
            {
                senderId = senderId,
                targetId = targetId,
                data = Convert.ToBase64String(payloadBytes),
                messageType = messageType,
                reliable = true
            };
        }

        private static P2PMessagePayload? ConvertToP2PMessagePayload(object content)
        {
            if (content is JObject jobj)
            {
                return jobj.ToObject<P2PMessagePayload>();
            }

            return Newtonsoft.Json.JsonConvert.DeserializeObject<P2PMessagePayload>(
                Newtonsoft.Json.JsonConvert.SerializeObject(content));
        }

        private static bool IsReliableChunk(P2PMessagePayload payload)
        {
            return !string.IsNullOrWhiteSpace(payload.reliableMessageId) &&
                   payload.reliableChunkIndex.HasValue &&
                   payload.reliableChunkCount.HasValue &&
                   payload.reliableTotalBytes.HasValue &&
                   payload.reliableMessageType.HasValue;
        }

        private byte[]? TryDecodeIncomingPayload(string remoteId, P2PMessagePayload payload, out uint resolvedMessageType)
        {
            resolvedMessageType = payload.messageType;
            if (string.IsNullOrWhiteSpace(payload.data))
            {
                return null;
            }

            if (!IsReliableChunk(payload))
            {
                resolvedMessageType = payload.messageType;
                return Convert.FromBase64String(payload.data);
            }

            try
            {
                var chunk = new IGPReliableChunk(
                    payload.reliableMessageId!,
                    payload.reliableChunkIndex!.Value,
                    payload.reliableChunkCount!.Value,
                    payload.reliableTotalBytes!.Value,
                    payload.reliableMessageType!.Value,
                    payload.targetId ?? string.Empty,
                    Convert.FromBase64String(payload.data));

                var completed = _reassembler.AddChunk(remoteId, chunk, DateTimeOffset.UtcNow);
                if (completed == null)
                {
                    return null;
                }

                resolvedMessageType = completed.MessageType;
                return completed.Payload;
            }
            catch (Exception ex)
            {
                Debug.LogWarning($"[IGPNetwork] Failed to reassemble reliable payload: {ex.Message}");
                return null;
            }
        }

        private void CleanupExpiredReliableMessages()
        {
            var expired = _reassembler.RemoveExpired(DateTimeOffset.UtcNow);
            foreach (var item in expired)
            {
                Debug.LogWarning($"[IGPNetwork] Reliable message reassembly timed out: connection={item.ConnectionId}, messageId={item.MessageId}");
            }
        }
    }
}
