#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEngine;
using UnityEngine.Events;
#if UNITY_EDITOR
using UnityEditor;
#endif
using IGP.UnitySDK.Abstractions;
using IGP.UnitySDK.Models;
using IGP.UnitySDK.Core;
using IGP.UnitySDK.Network;
using IGP.UnitySDK.Protocol;

namespace IGP.UnitySDK
{
    public partial class IGPRuntimeManager
    {

        private void ApplyLogSettings()
        {
            var level = LogLevel;
            IGPLog.Configure(level);
            appliedLogLevel = level;
            if (Network != null)
            {
                Network.LogLevel = level;
            }

            if (WebSocketClient != null)
            {
                WebSocketClient.LogLevel = level;
            }

            if (KcpClient != null)
            {
                KcpClient.LogLevel = level;
            }

            if (UnreliableUdpClient != null)
            {
                UnreliableUdpClient.LogLevel = level;
            }

            if (hostSessionClient != null)
            {
                hostSessionClient.LogLevel = level;
            }
        }

        private void ApplyLogSettingsIfNeeded()
        {
            if (appliedLogLevel != LogLevel)
            {
                ApplyLogSettings();
            }
        }

        private static int NormalizeKcpMaxDatagramsPerTick(int value) => Math.Max(1, value);

        private static int NormalizeKcpWindowSize(int value)
        {
            return Math.Min(IGPKcpClient.MaxWindowSize, Math.Max(IGPKcpClient.MinWindowSize, value));
        }

        private void LogHostedDataPlaneEvent(
            string eventName,
            int attemptId = 0,
            string errorClass = "",
            string detail = "")
        {
            var target = string.IsNullOrWhiteSpace(currentHostedDataPlaneHost) || currentHostedDataPlanePort == 0
                ? "<unresolved>"
                : $"{currentHostedDataPlaneHost}:{currentHostedDataPlanePort}";
            LogSdkInfo(
                "hosted-data-plane",
                $"event={eventName} roomId={currentRoomId} playerId={playerId} target={target} " +
                $"expiresAtUnixMs={currentHostedDataPlaneExpiresAtUnixMs} kcpAttemptId={attemptId} " +
                $"state={hostedConnectionState} status={currentHostedDataPlaneStatus} " +
                $"errorClass={FormatNetworkLogValue(errorClass)} {detail}");
        }

        private void LogSessionStart()
        {
            var appId = ResolveDesktopAppId(null);
            LogSdkInfo(
                "session",
                $"event=session-start role=unity-client " +
                $"localPlayerId={FormatNetworkLogValue(playerId)} roomId={FormatNetworkLogValue(currentRoomId)} " +
                $"appId={(appId.HasValue ? appId.Value.ToString() : "-")} sdkVersion={FormatNetworkLogValue(ResolveSdkVersion())} " +
                $"appVersion={FormatNetworkLogValue(Application.version)} unityVersion={FormatNetworkLogValue(Application.unityVersion)} " +
                $"platform={FormatNetworkLogValue(Environment.OSVersion.Platform.ToString())} " +
                $"transport=desktop-session endpoint={FormatNetworkLogValue(DesktopPipeEndpoint)} " +
                $"hostedSessionEndpoint={FormatNetworkLogValue(currentHostedSessionEndpoint)} dataPlaneMode={currentDataPlaneMode}");
        }

        private void LogSdkInfo(string path, string message)
        {
            LogSdkInfo("hosted", path, message);
        }

        private void LogSdkWarning(string path, string message)
        {
            LogSdkWarning("hosted", path, message);
        }

        private void LogSdkError(string path, string message)
        {
            LogSdkError("hosted", path, message);
        }

        private void LogSdkInfo(string scope, string path, string message)
        {
            IGPLog.Info(scope, path, message);
        }

        private void LogSdkWarning(string scope, string path, string message)
        {
            IGPLog.Warning(scope, path, message);
        }

        private void LogSdkError(string scope, string path, string message)
        {
            IGPLog.Error(scope, path, message);
        }

        private static string FormatNetworkLogValue(string? value)
        {
            return IGPLog.FormatValue(value);
        }

        private void OnGUI()
        {
            if (ShowNetworkDiagnostics)
            {
                var appStats = AppRTTStats;
                var liveTransportStats = KcpTransportStats;
                var transportStats = GetRttStatsOverlaySnapshot(liveTransportStats);
                bool hasAppRtt = appStats != null && appStats.SampleCount > 0;
                GUI.Box(new Rect(10, 10, 380, 238), "IGP Network Diagnostics");

                DrawRttStatsRow(20, 30, "KCP health:",
                    KcpHealthState.ToString());
                DrawRttStatsRow(20, 52, "Last fault:",
                    KcpLastFaultReason.ToString());

                if (hasAppRtt)
                {
                    DrawRttStatsRow(20, 74, "App RTT:",
                        $"avg {appStats!.AvgRTT * 1000:F0}ms",
                        $"last {appStats.LastRTT * 1000:F0}ms",
                        $"max {appStats.MaxRTT * 1000:F0}ms");
                }
                else
                {
                    DrawRttStatsRow(20, 74, "App RTT:", "measuring");
                }

                DrawRttStatsRow(20, 96, "KCP RTT:",
                    transportStats.HasSmoothedRtt ? $"srtt {transportStats.SmoothedRttMs}ms" : "srtt --",
                    transportStats.IsAvailable ? $"rto {transportStats.RtoMs}ms" : "rto --");

                DrawRttStatsRow(20, 118, "Queue:",
                    transportStats.IsAvailable && hasRttStatsOverlayQueueAverage ? $"wait {rttStatsOverlayQueueAverageWaitSnd}" : "wait --",
                    transportStats.IsAvailable && hasRttStatsOverlayQueueAverage ? $"q {rttStatsOverlayQueueAverageSndQueue}" : "q --",
                    transportStats.IsAvailable && hasRttStatsOverlayQueueAverage ? $"buf {rttStatsOverlayQueueAverageSndBuf}" : "buf --");

                DrawRttStatsRow(20, 140, "Q peak:",
                    transportStats.IsAvailable && hasRttStatsOverlayQueuePeak ? $"wait {rttStatsOverlayQueuePeakWaitSnd}" : "wait --",
                    transportStats.IsAvailable && hasRttStatsOverlayQueuePeak ? $"q {rttStatsOverlayQueuePeakSndQueue}" : "q --",
                    transportStats.IsAvailable && hasRttStatsOverlayQueuePeak ? $"buf {rttStatsOverlayQueuePeakSndBuf}" : "buf --");

                DrawRttStatsRow(20, 162, "UDP:",
                    transportStats.IsAvailable ? $"in {transportStats.DatagramsInPerSecond:F0}/s" : "in --",
                    transportStats.IsAvailable ? $"out {transportStats.DatagramsOutPerSecond:F0}/s" : "out --");

                DrawRttStatsRow(20, 184, "UDP pkts:",
                    transportStats.IsAvailable ? $"in {IGPRTTDisplayFormatting.FormatUdpDatagramCount(transportStats.DatagramsIn)}" : "in --",
                    transportStats.IsAvailable ? $"out {IGPRTTDisplayFormatting.FormatUdpDatagramCount(transportStats.DatagramsOut)}" : "out --");

                DrawRttStatsRow(20, 206, "UDP bytes:",
                    transportStats.IsAvailable ? $"in {IGPRTTDisplayFormatting.FormatByteCount(transportStats.DatagramBytesIn)}" : "in --",
                    transportStats.IsAvailable ? $"out {IGPRTTDisplayFormatting.FormatByteCount(transportStats.DatagramBytesOut)}" : "out --");
            }

            RenderAuthorizationFailureFallback();
        }

        private Core.IGPKcpTransportStats GetRttStatsOverlaySnapshot(Core.IGPKcpTransportStats liveStats)
        {
            if (!liveStats.IsAvailable)
            {
                ResetRttStatsOverlaySnapshot();
                return liveStats;
            }

            float now = Time.unscaledTime;
            RecordRttStatsOverlayQueueSample(now, liveStats);

            if (!hasRttStatsOverlaySnapshot || now >= nextRttStatsOverlayRefreshAt)
            {
                rttStatsOverlaySnapshot = liveStats;
                hasRttStatsOverlaySnapshot = true;
                nextRttStatsOverlayRefreshAt = now + RttStatsOverlayRefreshIntervalSeconds;
            }

            return rttStatsOverlaySnapshot;
        }

        private void RecordRttStatsOverlayQueueSample(float now, Core.IGPKcpTransportStats stats)
        {
            if (rttStatsOverlayQueueLastSampleAt > now)
            {
                rttStatsOverlayQueueSamples.Clear();
            }

            if (rttStatsOverlayQueueSamples.Count == 0 || now > rttStatsOverlayQueueLastSampleAt)
            {
                rttStatsOverlayQueueSamples.Enqueue(new RttStatsOverlayQueueSample(
                    now,
                    stats.WaitSnd,
                    stats.SndQueue,
                    stats.SndBuf));
                rttStatsOverlayQueueLastSampleAt = now;
            }

            UpdateRttStatsOverlayQueueWindow(now);
        }

        private void UpdateRttStatsOverlayQueueWindow(float now)
        {
            float peakCutoff = now - RttStatsOverlayQueuePeakWindowSeconds;
            while (rttStatsOverlayQueueSamples.Count > 0 &&
                   rttStatsOverlayQueueSamples.Peek().Timestamp < peakCutoff)
            {
                rttStatsOverlayQueueSamples.Dequeue();
            }

            float averageCutoff = now - RttStatsOverlayQueueAverageWindowSeconds;
            int averageCount = 0;
            long averageWaitSnd = 0;
            long averageSndQueue = 0;
            long averageSndBuf = 0;
            int peakWaitSnd = 0;
            int peakSndQueue = 0;
            int peakSndBuf = 0;

            foreach (var sample in rttStatsOverlayQueueSamples)
            {
                peakWaitSnd = Math.Max(peakWaitSnd, sample.WaitSnd);
                peakSndQueue = Math.Max(peakSndQueue, sample.SndQueue);
                peakSndBuf = Math.Max(peakSndBuf, sample.SndBuf);

                if (sample.Timestamp < averageCutoff)
                {
                    continue;
                }

                averageCount++;
                averageWaitSnd += sample.WaitSnd;
                averageSndQueue += sample.SndQueue;
                averageSndBuf += sample.SndBuf;
            }

            hasRttStatsOverlayQueuePeak = rttStatsOverlayQueueSamples.Count > 0;
            rttStatsOverlayQueuePeakWaitSnd = peakWaitSnd;
            rttStatsOverlayQueuePeakSndQueue = peakSndQueue;
            rttStatsOverlayQueuePeakSndBuf = peakSndBuf;

            hasRttStatsOverlayQueueAverage = averageCount > 0;
            if (averageCount > 0)
            {
                rttStatsOverlayQueueAverageWaitSnd = (int)Math.Round((double)averageWaitSnd / averageCount);
                rttStatsOverlayQueueAverageSndQueue = (int)Math.Round((double)averageSndQueue / averageCount);
                rttStatsOverlayQueueAverageSndBuf = (int)Math.Round((double)averageSndBuf / averageCount);
            }
        }

        private void ResetRttStatsOverlaySnapshot()
        {
            hasRttStatsOverlaySnapshot = false;
            rttStatsOverlaySnapshot = Core.IGPKcpTransportStats.Unavailable;
            nextRttStatsOverlayRefreshAt = 0f;
            hasRttStatsOverlayQueueAverage = false;
            rttStatsOverlayQueueAverageWaitSnd = 0;
            rttStatsOverlayQueueAverageSndQueue = 0;
            rttStatsOverlayQueueAverageSndBuf = 0;
            hasRttStatsOverlayQueuePeak = false;
            rttStatsOverlayQueuePeakWaitSnd = 0;
            rttStatsOverlayQueuePeakSndQueue = 0;
            rttStatsOverlayQueuePeakSndBuf = 0;
            rttStatsOverlayQueueSamples.Clear();
            rttStatsOverlayQueueLastSampleAt = -1f;
        }

        private readonly struct RttStatsOverlayQueueSample
        {
            public RttStatsOverlayQueueSample(float timestamp, int waitSnd, int sndQueue, int sndBuf)
            {
                Timestamp = timestamp;
                WaitSnd = waitSnd;
                SndQueue = sndQueue;
                SndBuf = sndBuf;
            }

            public float Timestamp { get; }
            public int WaitSnd { get; }
            public int SndQueue { get; }
            public int SndBuf { get; }
        }

        private static void DrawRttStatsRow(float x, float y, string title, string first, string? second = null, string? third = null)
        {
            const float lineHeight = 20f;
            const float titleWidth = 76f;
            const float columnWidth = 96f;

            GUI.Label(new Rect(x, y, titleWidth, lineHeight), title);
            float firstWidth = string.IsNullOrEmpty(second) && string.IsNullOrEmpty(third)
                ? columnWidth * 3
                : columnWidth;
            GUI.Label(new Rect(x + titleWidth, y, firstWidth, lineHeight), first);

            if (!string.IsNullOrEmpty(second))
            {
                GUI.Label(new Rect(x + titleWidth + columnWidth, y, columnWidth, lineHeight), second);
            }

            if (!string.IsNullOrEmpty(third))
            {
                GUI.Label(new Rect(x + titleWidth + columnWidth * 2, y, columnWidth, lineHeight), third);
            }
        }
    }
}
