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

namespace IGP.Multiplayer.Core
{
    internal sealed class IGPUdpClient : IDisposable
    {
        public const int DefaultPayloadMaxBytes = 1200;

        private const string LogScope = "udp";
        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 bool useEnvelopeV1;
        private IGPDataPlanePeerTable? peerTable;
        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; }

        internal event Action<IGPTransportMessageReceived>? MessageReceived;
        internal event Action<IGPTransportConnectionChanged>? ConnectionChanged;
        internal event Action<IGPTransportFault>? Faulted;

        public void Connect(string host, int port, string roomId, string playerId, string token, int payloadMaxBytes, bool useEnvelopeV1 = false)
        {
            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;
            this.useEnvelopeV1 = useEnvelopeV1;
            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}");
                Faulted?.Invoke(new IGPTransportFault(IGPRealtimeTransport.Udp, "UDP_CONNECT_FAILED", $"UDP connect failed: {ex.Message}", false, ex));
                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;
            useEnvelopeV1 = false;
            peerTable = null;
            RoomId = null;
            PlayerId = null;
            lastHelloSentAtUnixMs = 0;
            ResetTrafficDiagnostics();
            if (hadState)
            {
                ConnectionChanged?.Invoke(new IGPTransportConnectionChanged(IGPRealtimeTransport.Udp, false, "disconnected"));
                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}");
                Faulted?.Invoke(new IGPTransportFault(IGPRealtimeTransport.Udp, "UDP_RECEIVE_FAILED", $"UDP receive failed: {ex.Message}", false, ex));
            }

            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;
                return true;
            }
            catch (Exception ex)
            {
                LogWarning("send", $"event=failed packetBytes={packet.Length} error={ex.Message}");
                Faulted?.Invoke(new IGPTransportFault(IGPRealtimeTransport.Udp, "UDP_SEND_FAILED", $"UDP send failed: {ex.Message}", false, ex));
                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 + (useEnvelopeV1 ? 1 : 0)];
            WriteHeader(packet, PacketHello);
            BinaryPrimitives.WriteUInt16BigEndian(packet.AsSpan(HeaderLength, 2), (ushort)tokenBytes.Length);
            Buffer.BlockCopy(tokenBytes, 0, packet, HeaderLength + 2, tokenBytes.Length);
            if (useEnvelopeV1)
            {
                packet[packet.Length - 1] = IGPDataPlaneEnvelopeCodec.Version;
            }
            int sent = socket.Send(packet);
            if (sent > 0)
            {
                totalDatagramsSent += 1;
                totalDatagramBytesSent += sent;
            }
            lastHelloSentAtUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
        }

        private void HandlePacket(byte[] packet)
        {
            if (!TryReadHeader(packet, out var kind, out var body))
            {
                return;
            }

            switch (kind)
            {
                case PacketAck:
                    HandleAck(body);
                    break;
                case PacketData:
                    HandleData(body);
                    break;
                default:
                    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);
            }
            int selectedVersionOffset = body.Offset + 2 + sessionLen + 4 + 8;
            if (useEnvelopeV1 &&
                (body.Count <= selectedVersionOffset - body.Offset || body.Array![selectedVersionOffset] != IGPDataPlaneEnvelopeCodec.Version))
            {
                LogWarning("handshake", "event=ack-dropped reason=envelope-v1-not-confirmed");
                return;
            }

            if (useEnvelopeV1)
            {
                int cursor = selectedVersionOffset + 1;
                int end = body.Offset + body.Count;
                if (cursor + 3 > end) { LogWarning("handshake", "event=ack-dropped reason=slot-metadata-truncated"); return; }
                byte playerSlot = body.Array![cursor++];
                byte hostSlot = body.Array[cursor++];
                int count = body.Array[cursor++];
                if (playerSlot < 1 || playerSlot > 64 || hostSlot != 1 || count < 1 || count > 64)
                {
                    LogWarning("handshake", "event=ack-dropped reason=slot-metadata-invalid");
                    return;
                }
                var nextSlotByPlayerId = new Dictionary<string, byte>(StringComparer.Ordinal);
                var nextPlayerIdBySlot = new Dictionary<byte, string>();
                for (int i = 0; i < count; i++)
                {
                    if (cursor + 3 > end) { LogWarning("handshake", "event=ack-dropped reason=roster-truncated"); return; }
                    byte slot = body.Array[cursor++];
                    int idLength = BinaryPrimitives.ReadUInt16BigEndian(body.Array.AsSpan(cursor, 2)); cursor += 2;
                    if (slot < 1 || slot > 64 || idLength <= 0 || cursor + idLength > end) { LogWarning("handshake", "event=ack-dropped reason=roster-entry-invalid"); return; }
                    string id = Encoding.UTF8.GetString(body.Array, cursor, idLength); cursor += idLength;
                    if (string.IsNullOrWhiteSpace(id) || nextSlotByPlayerId.ContainsKey(id) || nextPlayerIdBySlot.ContainsKey(slot))
                    {
                        LogWarning("handshake", "event=ack-dropped reason=roster-duplicate");
                        return;
                    }
                    nextSlotByPlayerId.Add(id, slot); nextPlayerIdBySlot.Add(slot, id);
                }
                if (cursor != end || !nextPlayerIdBySlot.TryGetValue(playerSlot, out string? localPlayerId) ||
                    !string.Equals(localPlayerId, PlayerId, StringComparison.Ordinal) || !nextPlayerIdBySlot.ContainsKey(hostSlot))
                {
                    LogWarning("handshake", "event=ack-dropped reason=roster-session-mismatch");
                    return;
                }
                peerTable = new IGPDataPlanePeerTable(
                    playerSlot,
                    hostSlot,
                    nextSlotByPlayerId,
                    nextPlayerIdBySlot);
            }

            bool wasConnected = IsConnected;
            sessionId = nextSessionId;
            token = string.Empty;
            if (!wasConnected)
            {
                ConnectionChanged?.Invoke(new IGPTransportConnectionChanged(IGPRealtimeTransport.Udp, true, "handshake-ack"));
            }

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

        private void HandleData(ArraySegment<byte> body)
        {
            if (!TryDecodeData(body, out var receivedSessionId, out var envelopeBytes))
            {
                return;
            }

            if (!string.Equals(receivedSessionId, sessionId, StringComparison.Ordinal))
            {
                return;
            }

            try
            {
                if (useEnvelopeV1)
                {
                    HandleCommonDataEnvelope(envelopeBytes);
                    return;
                }
                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 IGPTransportMessageReceived(IGPRealtimeTransport.Udp, 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}");
                Faulted?.Invoke(new IGPTransportFault(IGPRealtimeTransport.Udp, "UDP_ENVELOPE_PARSE_FAILED", $"Failed to parse UDP envelope: {ex.Message}", false, ex));
            }
        }

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

            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
            {
                if (useEnvelopeV1)
                {
                    if (peerTable == null) return false;
                    var envelopeMessage = new Message
                    {
                        type = message.type,
                        roomId = message.roomId,
                        playerId = message.playerId,
                        targetPlayerId = message.targetPlayerId,
                        reliable = false,
                        content = payload,
                        dataPlaneTargetPlayerIds = message.dataPlaneTargetPlayerIds,
                    };
                    envelopeBytes = IGPDataPlaneMessageCodec.Encode(envelopeMessage, peerTable);
                    return envelopeBytes.Length <= PayloadMaxBytes;
                }
                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 void HandleCommonDataEnvelope(byte[] wire)
        {
            if (peerTable == null) throw new InvalidOperationException("UDP peer table is unavailable.");
            Message message = IGPDataPlaneMessageCodec.Decode(
                wire,
                RoomId ?? string.Empty,
                IGPRealtimeTransport.Udp,
                peerTable);
            MessageReceived?.Invoke(new IGPTransportMessageReceived(IGPRealtimeTransport.Udp, message));
        }

        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=udp 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 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);
        }
    }
}
