#nullable enable
using System;
using System.Buffers.Binary;
using System.Net;
using System.Net.Sockets;
using System.Text;
using IGP.UnitySDK.Models;
using IGP.UnitySDK.Network;
using IGP.UnitySDK.Protocol;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEngine;

namespace IGP.UnitySDK.Core
{
    public sealed class IGPUnreliableUdpClient : IDisposable
    {
        public const int DefaultPayloadMaxBytes = 1200;

        private const string LogScope = "udp-unreliable";
        private const byte WireVersion = 1;
        private const byte PacketHello = 1;
        private const byte PacketAck = 2;
        private const byte PacketData = 3;
        private const int HeaderLength = 6;
        private const int ReceiveBufferBytes = 64 * 1024;
        private const long HelloRetryIntervalMs = 1000;
        private const float TrafficRateSampleIntervalSeconds = 0.5f;

        private readonly byte[] receiveBuffer = new byte[ReceiveBufferBytes];
        private readonly IGPNetworkAnomalyLogLimiter anomalyLogLimiter = new IGPNetworkAnomalyLogLimiter();
        private Socket? socket;
        private EndPoint? remoteEndPoint;
        private string token = string.Empty;
        private string sessionId = string.Empty;
        private long lastHelloSentAtUnixMs;
        private bool isDisposed;
        private long totalDatagramsReceived;
        private long totalDatagramsSent;
        private long totalDatagramBytesReceived;
        private long totalDatagramBytesSent;
        private long lastRateDatagramsReceived;
        private long lastRateDatagramsSent;
        private long lastRateDatagramBytesReceived;
        private long lastRateDatagramBytesSent;
        private float lastTrafficRateSampleAt;
        private float datagramsReceivedPerSecond;
        private float datagramsSentPerSecond;
        private float datagramBytesReceivedPerSecond;
        private float datagramBytesSentPerSecond;

        public string? RoomId { get; private set; }
        public string? PlayerId { get; private set; }
        public int PayloadMaxBytes { get; private set; } = DefaultPayloadMaxBytes;
        public bool IsConnected => socket != null && !string.IsNullOrEmpty(sessionId);
        public IGPLogLevel LogLevel { get; set; }
        public bool DebugLogging
        {
            get => LogLevel != IGPLogLevel.Off;
            set => LogLevel = value ? IGPLogLevel.Debug : IGPLogLevel.Off;
        }

        public event Action<Message>? MessageReceived;
        public event Action<string>? ErrorOccurred;
        public event Action<bool>? ConnectionStateChanged;

        public void Connect(string host, int port, string roomId, string playerId, string token, int payloadMaxBytes)
        {
            if (string.IsNullOrWhiteSpace(host))
            {
                throw new ArgumentException("UDP host is required.", nameof(host));
            }

            if (port <= 0 || port > ushort.MaxValue)
            {
                throw new ArgumentOutOfRangeException(nameof(port));
            }

            if (string.IsNullOrWhiteSpace(roomId))
            {
                throw new ArgumentException("Room id is required.", nameof(roomId));
            }

            if (string.IsNullOrWhiteSpace(playerId))
            {
                throw new ArgumentException("Player id is required.", nameof(playerId));
            }

            if (string.IsNullOrWhiteSpace(token))
            {
                throw new ArgumentException("UDP token is required.", nameof(token));
            }

            Disconnect();

            RoomId = roomId;
            PlayerId = playerId;
            this.token = token;
            PayloadMaxBytes = payloadMaxBytes > 0 ? payloadMaxBytes : DefaultPayloadMaxBytes;

            try
            {
                var ip = ResolveIP(host);
                remoteEndPoint = new IPEndPoint(ip, port);
                socket = new Socket(ip.AddressFamily, SocketType.Dgram, ProtocolType.Udp)
                {
                    Blocking = false
                };
                socket.Connect(remoteEndPoint);

                SendHello();
                LogInfo("connect", $"event=start targetHost={host} resolvedIP={ip} port={port} roomId={roomId} localPlayerId={playerId}");
            }
            catch (Exception ex)
            {
                LogError("connect", $"event=failed targetHost={host} port={port} roomId={roomId} localPlayerId={playerId} error={ex.Message}");
                ErrorOccurred?.Invoke($"Unreliable UDP connect failed: {ex.Message}");
                Disconnect();
            }
        }

        public void Disconnect()
        {
            bool hadState = socket != null || !string.IsNullOrEmpty(sessionId) || !string.IsNullOrEmpty(RoomId) || !string.IsNullOrEmpty(PlayerId);
            try
            {
                socket?.Close(0);
            }
            catch
            {
                // ignore
            }

            socket = null;
            remoteEndPoint = null;
            token = string.Empty;
            sessionId = string.Empty;
            RoomId = null;
            PlayerId = null;
            lastHelloSentAtUnixMs = 0;
            ResetTrafficDiagnostics();
            if (hadState)
            {
                ConnectionStateChanged?.Invoke(false);
                LogInfo("disconnect", "event=complete");
            }
        }

        public void Dispose()
        {
            if (isDisposed)
            {
                return;
            }

            isDisposed = true;
            Disconnect();
        }

        public void Tick()
        {
            if (socket == null)
            {
                return;
            }

            RetryHelloIfNeeded();

            try
            {
                while (socket.Poll(0, SelectMode.SelectRead))
                {
                    int received = socket.Receive(receiveBuffer, 0, receiveBuffer.Length, SocketFlags.None);
                    if (received <= 0)
                    {
                        break;
                    }

                    totalDatagramsReceived += 1;
                    totalDatagramBytesReceived += received;
                    var packet = new byte[received];
                    Buffer.BlockCopy(receiveBuffer, 0, packet, 0, received);
                    HandlePacket(packet);
                }
            }
            catch (SocketException)
            {
                // Non-blocking sockets may report transient receive state; ignore.
            }
            catch (Exception ex)
            {
                LogWarning("recv", $"event=failed error={ex.Message}");
                ErrorOccurred?.Invoke($"Unreliable UDP receive failed: {ex.Message}");
            }

            UpdateTrafficRateAndWarnings();
        }

        public bool TrySendMessage(Message message)
        {
            if (!IsConnected || socket == null || string.IsNullOrEmpty(RoomId) || string.IsNullOrEmpty(PlayerId))
            {
                LogWarning("send", $"event=dropped reason=not-ready connected={IsConnected} type={message?.type ?? string.Empty}");
                return false;
            }

            if (!TryEncodeMessage(message, out var envelope))
            {
                return false;
            }

            var packet = EncodeData(sessionId, envelope);
            try
            {
                int sent = socket.Send(packet);
                if (sent <= 0)
                {
                    LogWarning("send", $"event=dropped reason=socket-returned-zero packetBytes={packet.Length}");
                    return false;
                }

                totalDatagramsSent += 1;
                totalDatagramBytesSent += sent;
                if (ShouldLog(IGPLogLevel.Debug))
                {
                    LogDebug("send", $"event=sent packetBytes={packet.Length} envelopeBytes={envelope.Length}");
                }

                return true;
            }
            catch (Exception ex)
            {
                LogWarning("send", $"event=failed packetBytes={packet.Length} error={ex.Message}");
                ErrorOccurred?.Invoke($"Unreliable UDP send failed: {ex.Message}");
                return false;
            }
        }

        public void SendMessage(Message message)
        {
            _ = TrySendMessage(message);
        }

        private void RetryHelloIfNeeded()
        {
            if (IsConnected || string.IsNullOrWhiteSpace(token))
            {
                return;
            }

            long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
            if (lastHelloSentAtUnixMs > 0 && now - lastHelloSentAtUnixMs < HelloRetryIntervalMs)
            {
                return;
            }

            SendHello();
        }

        private void SendHello()
        {
            if (socket == null)
            {
                return;
            }

            var tokenBytes = Encoding.UTF8.GetBytes(token);
            if (tokenBytes.Length == 0 || tokenBytes.Length > ushort.MaxValue)
            {
                throw new InvalidOperationException("UDP token length is invalid.");
            }

            var packet = new byte[HeaderLength + 2 + tokenBytes.Length];
            WriteHeader(packet, PacketHello);
            BinaryPrimitives.WriteUInt16BigEndian(packet.AsSpan(HeaderLength, 2), (ushort)tokenBytes.Length);
            Buffer.BlockCopy(tokenBytes, 0, packet, HeaderLength + 2, tokenBytes.Length);
            int sent = socket.Send(packet);
            if (sent > 0)
            {
                totalDatagramsSent += 1;
                totalDatagramBytesSent += sent;
            }
            lastHelloSentAtUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
            LogDebug("handshake", $"event=hello-sent tokenBytes={tokenBytes.Length}");
        }

        private void HandlePacket(byte[] packet)
        {
            if (!TryReadHeader(packet, out var kind, out var body))
            {
                LogDebug("recv", $"event=dropped reason=bad-header packetBytes={packet.Length}");
                return;
            }

            switch (kind)
            {
                case PacketAck:
                    HandleAck(body);
                    break;
                case PacketData:
                    HandleData(body);
                    break;
                default:
                    LogDebug("recv", $"event=dropped reason=unsupported-kind kind={kind} packetBytes={packet.Length}");
                    break;
            }
        }

        private void HandleAck(ArraySegment<byte> body)
        {
            if (body.Count < 2 + 4 + 8)
            {
                LogWarning("handshake", "event=ack-dropped reason=truncated");
                return;
            }

            int offset = body.Offset;
            int sessionLen = BinaryPrimitives.ReadUInt16BigEndian(body.Array!.AsSpan(offset, 2));
            offset += 2;
            if (sessionLen <= 0 || body.Count < 2 + sessionLen + 4 + 8)
            {
                LogWarning("handshake", $"event=ack-dropped reason=invalid-session sessionBytes={sessionLen}");
                return;
            }

            string nextSessionId = Encoding.UTF8.GetString(body.Array!, offset, sessionLen);
            offset += sessionLen;
            uint negotiatedMax = BinaryPrimitives.ReadUInt32BigEndian(body.Array!.AsSpan(offset, 4));
            if (negotiatedMax > 0)
            {
                int negotiatedMaxBytes = negotiatedMax > int.MaxValue ? int.MaxValue : (int)negotiatedMax;
                PayloadMaxBytes = Math.Min(PayloadMaxBytes, negotiatedMaxBytes);
            }

            bool wasConnected = IsConnected;
            sessionId = nextSessionId;
            token = string.Empty;
            if (!wasConnected)
            {
                ConnectionStateChanged?.Invoke(true);
            }

            LogInfo("handshake", $"event=ack sessionId={sessionId} payloadMaxBytes={PayloadMaxBytes}");
        }

        private void HandleData(ArraySegment<byte> body)
        {
            if (!TryDecodeData(body, out var receivedSessionId, out var envelopeBytes))
            {
                LogDebug("recv", $"event=dropped reason=bad-data bodyBytes={body.Count}");
                return;
            }

            if (!string.Equals(receivedSessionId, sessionId, StringComparison.Ordinal))
            {
                LogDebug("recv", $"event=dropped reason=session-mismatch sessionId={receivedSessionId}");
                return;
            }

            try
            {
                var envelope = IGPKcpBinaryEnvelopeCodec.Decode(envelopeBytes);
                if (envelope.Version != IGPKcpBinaryEnvelopeCodec.Version1 || envelope.IsReliableChunk)
                {
                    LogWarning("recv", $"event=dropped reason=unsupported-envelope version={envelope.Version} reliableChunk={envelope.IsReliableChunk}");
                    return;
                }

                var payload = new P2PMessagePayload
                {
                    senderId = envelope.SenderPlayerId,
                    targetId = envelope.TargetPlayerId,
                    data = Convert.ToBase64String(envelope.Payload),
                    messageType = envelope.MessageType,
                    transportChannel = IGPP2PTransportChannels.Data,
                    reliable = false
                };

                MessageReceived?.Invoke(new Message
                {
                    type = "p2p_data",
                    roomId = RoomId ?? string.Empty,
                    playerId = envelope.SenderPlayerId,
                    targetPlayerId = envelope.TargetPlayerId,
                    reliable = false,
                    content = payload
                });
            }
            catch (Exception ex)
            {
                LogWarning("recv", $"event=envelope-parse-failed bytes={envelopeBytes.Length} error={ex.Message}");
                ErrorOccurred?.Invoke($"Failed to parse unreliable UDP envelope: {ex.Message}");
            }
        }

        private bool TryEncodeMessage(Message message, out byte[] envelopeBytes)
        {
            envelopeBytes = Array.Empty<byte>();
            if (!string.Equals(message.type, "p2p_data", StringComparison.Ordinal) || message.content == null)
            {
                LogWarning("send", $"event=dropped reason=unsupported-message-type type={message.type}");
                return false;
            }

            P2PMessagePayload? payload;
            if (message.content is P2PMessagePayload typedPayload)
            {
                payload = typedPayload;
            }
            else if (message.content is JObject jobj)
            {
                payload = jobj.ToObject<P2PMessagePayload>();
            }
            else
            {
                payload = JsonConvert.DeserializeObject<P2PMessagePayload>(
                    JsonConvert.SerializeObject(message.content));
            }

            if (payload == null || string.IsNullOrWhiteSpace(payload.data))
            {
                LogWarning("send", "event=dropped reason=missing-payload");
                return false;
            }

            if (payload.transportSequence.HasValue ||
                !string.Equals(IGPP2PTransportChannels.Resolve(payload.transportChannel, false), IGPP2PTransportChannels.Data, StringComparison.Ordinal) ||
                HasReliableChunkMetadata(payload))
            {
                LogWarning("send", "event=dropped reason=unsupported-payload-shape");
                return false;
            }

            byte[] rawPayload;
            try
            {
                rawPayload = Convert.FromBase64String(payload.data);
            }
            catch
            {
                LogWarning("send", "event=dropped reason=invalid-base64");
                return false;
            }

            if (rawPayload.Length <= 0 || rawPayload.Length > PayloadMaxBytes)
            {
                LogWarning("send", $"event=dropped reason=payload-too-large bytes={rawPayload.Length} limit={PayloadMaxBytes}");
                return false;
            }

            string senderPlayerId = !string.IsNullOrWhiteSpace(payload.senderId)
                ? payload.senderId
                : message.playerId ?? PlayerId ?? string.Empty;
            string targetPlayerId = payload.targetId ?? message.targetPlayerId ?? string.Empty;
            var targetKind = string.IsNullOrWhiteSpace(targetPlayerId)
                ? IGPKcpTargetKind.Broadcast
                : IGPKcpTargetKind.Player;

            try
            {
                envelopeBytes = IGPKcpBinaryEnvelopeCodec.Encode(new IGPKcpBinaryEnvelope(
                    version: IGPKcpBinaryEnvelopeCodec.Version1,
                    flags: 0,
                    messageType: payload.messageType,
                    targetKind: targetKind,
                    senderPlayerId: senderPlayerId,
                    targetPlayerId: targetPlayerId,
                    payload: rawPayload));
                return true;
            }
            catch (Exception ex)
            {
                LogWarning("send", $"event=dropped reason=encode-failed error={ex.Message}");
                return false;
            }
        }

        private static bool TryReadHeader(byte[] packet, out byte kind, out ArraySegment<byte> body)
        {
            kind = 0;
            body = default;
            if (packet.Length < HeaderLength ||
                packet[0] != (byte)'I' ||
                packet[1] != (byte)'G' ||
                packet[2] != (byte)'P' ||
                packet[3] != (byte)'U' ||
                packet[4] != WireVersion)
            {
                return false;
            }

            kind = packet[5];
            body = new ArraySegment<byte>(packet, HeaderLength, packet.Length - HeaderLength);
            return true;
        }

        private static bool TryDecodeData(ArraySegment<byte> body, out string decodedSessionId, out byte[] envelope)
        {
            decodedSessionId = string.Empty;
            envelope = Array.Empty<byte>();
            if (body.Count < 3)
            {
                return false;
            }

            int offset = body.Offset;
            int sessionLen = BinaryPrimitives.ReadUInt16BigEndian(body.Array!.AsSpan(offset, 2));
            offset += 2;
            if (sessionLen <= 0 || body.Count <= 2 + sessionLen)
            {
                return false;
            }

            decodedSessionId = Encoding.UTF8.GetString(body.Array!, offset, sessionLen);
            offset += sessionLen;
            int envelopeLength = body.Offset + body.Count - offset;
            envelope = new byte[envelopeLength];
            Buffer.BlockCopy(body.Array!, offset, envelope, 0, envelopeLength);
            return true;
        }

        private static byte[] EncodeData(string sessionId, byte[] envelope)
        {
            var sessionBytes = Encoding.UTF8.GetBytes(sessionId);
            if (sessionBytes.Length == 0 || sessionBytes.Length > ushort.MaxValue)
            {
                throw new InvalidOperationException("UDP session id length is invalid.");
            }

            var packet = new byte[HeaderLength + 2 + sessionBytes.Length + envelope.Length];
            WriteHeader(packet, PacketData);
            BinaryPrimitives.WriteUInt16BigEndian(packet.AsSpan(HeaderLength, 2), (ushort)sessionBytes.Length);
            Buffer.BlockCopy(sessionBytes, 0, packet, HeaderLength + 2, sessionBytes.Length);
            Buffer.BlockCopy(envelope, 0, packet, HeaderLength + 2 + sessionBytes.Length, envelope.Length);
            return packet;
        }

        private static void WriteHeader(byte[] packet, byte kind)
        {
            packet[0] = (byte)'I';
            packet[1] = (byte)'G';
            packet[2] = (byte)'P';
            packet[3] = (byte)'U';
            packet[4] = WireVersion;
            packet[5] = kind;
        }

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

        private static IPAddress ResolveIP(string host)
        {
            if (IPAddress.TryParse(host, out var ip))
            {
                return ip;
            }

            var addresses = Dns.GetHostAddresses(host);
            if (addresses.Length == 0)
            {
                throw new InvalidOperationException($"Cannot resolve host: {host}");
            }

            foreach (var addr in addresses)
            {
                if (addr.AddressFamily == AddressFamily.InterNetwork)
                {
                    return addr;
                }
            }

            return addresses[0];
        }

        private void ResetTrafficDiagnostics()
        {
            totalDatagramsReceived = 0;
            totalDatagramsSent = 0;
            totalDatagramBytesReceived = 0;
            totalDatagramBytesSent = 0;
            lastRateDatagramsReceived = 0;
            lastRateDatagramsSent = 0;
            lastRateDatagramBytesReceived = 0;
            lastRateDatagramBytesSent = 0;
            lastTrafficRateSampleAt = 0f;
            datagramsReceivedPerSecond = 0f;
            datagramsSentPerSecond = 0f;
            datagramBytesReceivedPerSecond = 0f;
            datagramBytesSentPerSecond = 0f;
            anomalyLogLimiter.Reset();
        }

        private void UpdateTrafficRateAndWarnings()
        {
            float now = Time.realtimeSinceStartup;
            if (lastTrafficRateSampleAt <= 0f)
            {
                lastTrafficRateSampleAt = now;
                lastRateDatagramsReceived = totalDatagramsReceived;
                lastRateDatagramsSent = totalDatagramsSent;
                lastRateDatagramBytesReceived = totalDatagramBytesReceived;
                lastRateDatagramBytesSent = totalDatagramBytesSent;
                return;
            }

            float elapsed = now - lastTrafficRateSampleAt;
            if (elapsed < TrafficRateSampleIntervalSeconds)
            {
                return;
            }

            datagramsReceivedPerSecond = (totalDatagramsReceived - lastRateDatagramsReceived) / elapsed;
            datagramsSentPerSecond = (totalDatagramsSent - lastRateDatagramsSent) / elapsed;
            datagramBytesReceivedPerSecond = (totalDatagramBytesReceived - lastRateDatagramBytesReceived) / elapsed;
            datagramBytesSentPerSecond = (totalDatagramBytesSent - lastRateDatagramBytesSent) / elapsed;
            lastTrafficRateSampleAt = now;
            lastRateDatagramsReceived = totalDatagramsReceived;
            lastRateDatagramsSent = totalDatagramsSent;
            lastRateDatagramBytesReceived = totalDatagramBytesReceived;
            lastRateDatagramBytesSent = totalDatagramBytesSent;

            if (!ShouldLog(IGPLogLevel.Warning))
            {
                anomalyLogLimiter.Reset();
                return;
            }

            bool highTraffic = datagramsReceivedPerSecond >= IGPNetworkAnomalyThresholds.HighDatagramsPerSecond ||
                               datagramsSentPerSecond >= IGPNetworkAnomalyThresholds.HighDatagramsPerSecond ||
                               datagramBytesReceivedPerSecond >= IGPNetworkAnomalyThresholds.HighDatagramBytesPerSecond ||
                               datagramBytesSentPerSecond >= IGPNetworkAnomalyThresholds.HighDatagramBytesPerSecond;
            bool recovered = datagramsReceivedPerSecond <= IGPNetworkAnomalyThresholds.HighDatagramsPerSecondRecovery &&
                             datagramsSentPerSecond <= IGPNetworkAnomalyThresholds.HighDatagramsPerSecondRecovery &&
                             datagramBytesReceivedPerSecond <= IGPNetworkAnomalyThresholds.HighDatagramBytesPerSecondRecovery &&
                             datagramBytesSentPerSecond <= IGPNetworkAnomalyThresholds.HighDatagramBytesPerSecondRecovery;
            anomalyLogLimiter.ObserveContinuous(
                "high-traffic",
                highTraffic,
                recovered,
                now,
                () => $"transport=raw-udp-unreliable datagramsInPerSecond={datagramsReceivedPerSecond:F0} " +
                      $"datagramsOutPerSecond={datagramsSentPerSecond:F0} bytesInPerSecond={datagramBytesReceivedPerSecond:F0} " +
                      $"bytesOutPerSecond={datagramBytesSentPerSecond:F0} " +
                      $"datagramThresholdPerSecond={IGPNetworkAnomalyThresholds.HighDatagramsPerSecond:F0} " +
                      $"byteThresholdPerSecond={IGPNetworkAnomalyThresholds.HighDatagramBytesPerSecond:F0}",
                message => LogWarning("anomaly", message),
                message => LogInfo("anomaly", message));
        }

        private void LogDebug(string path, string message)
        {
            IGPLog.Debug(LogScope, path, message);
        }

        private void LogInfo(string path, string message)
        {
            IGPLog.Info(LogScope, path, message);
        }

        private void LogWarning(string path, string message)
        {
            IGPLog.Warning(LogScope, path, message);
        }

        private void LogError(string path, string message)
        {
            IGPLog.Error(LogScope, path, message);
        }

        private bool ShouldLog(IGPLogLevel messageLevel)
        {
            return IGPLog.ShouldLog(messageLevel);
        }
    }
}
