#nullable enable
using System;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading;
using UnityEngine;

namespace IGP.UnitySDK
{
    /// <summary>
    /// Runtime network diagnostics for short-lived KCP/data-plane bursts.
    /// </summary>
    public static class IGPNetworkDiagnostics
    {
        private static long producedMsgsServer;
        private static long producedBytesServer;
        private static long producedMsgsClient;
        private static long producedBytesClient;
        private static long outDatagrams;
        private static long outBytes;
        private static long recvDatagrams;
        private static long recvBytes;
        private static int recvHit64Count;

        private static int waitSnd = -1;
        private static int pendingPackets;
        private static long pendingPacketBytes;
        private static long droppedPackets;
        private static long droppedPacketBytes;
        private static int reassemblyBytes;
        private static int peakWaitSnd;
        private static float rttLastMs = -1f;
        private static float rttAvgMs = -1f;
        private static float rttRawAvgMs = -1f;
        private static int rttSampleCount;
        private static int rttCongestedSampleCount;

        private static float lastFlushTime;
        private static bool started;
        private static bool enabled;
        private static bool overlayEnabled;
        private static string currentSnapshot = "[NetDiag] disabled";

        public static string CurrentSnapshot => currentSnapshot;

        public static int WaitSnd => waitSnd;

        public static bool Enabled => enabled;

        public static bool OverlayEnabled => overlayEnabled;

        public static string LogFilePath
        {
            get
            {
                string gameDir = Directory.GetParent(Application.dataPath)?.FullName ?? Application.dataPath;
                return Path.Combine(gameDir, "Log", "Log.txt");
            }
        }

        public static void SetEnabled(bool value)
        {
            if (enabled == value)
            {
                return;
            }

            enabled = value;
            ResetCounters();
            if (enabled)
            {
                WriteHeader();
                if (overlayEnabled)
                {
                    IGPNetworkDiagnosticsOverlay.EnsureExists();
                }

                currentSnapshot = "[NetDiag] waiting for samples...";
            }
            else
            {
                currentSnapshot = "[NetDiag] disabled";
            }
        }

        public static void SetOverlayEnabled(bool value)
        {
            overlayEnabled = value;
            if (overlayEnabled && enabled)
            {
                IGPNetworkDiagnosticsOverlay.EnsureExists();
            }
        }

        public static void AddProducedServer(int bytes)
        {
            if (!enabled)
            {
                return;
            }

            Interlocked.Increment(ref producedMsgsServer);
            Interlocked.Add(ref producedBytesServer, Math.Max(0, bytes));
        }

        public static void AddProducedClient(int bytes)
        {
            if (!enabled)
            {
                return;
            }

            Interlocked.Increment(ref producedMsgsClient);
            Interlocked.Add(ref producedBytesClient, Math.Max(0, bytes));
        }

        public static void AddOutDatagram(int bytes)
        {
            if (!enabled)
            {
                return;
            }

            Interlocked.Increment(ref outDatagrams);
            Interlocked.Add(ref outBytes, Math.Max(0, bytes));
        }

        public static void AddRecvDatagram(int bytes)
        {
            if (!enabled)
            {
                return;
            }

            Interlocked.Increment(ref recvDatagrams);
            Interlocked.Add(ref recvBytes, Math.Max(0, bytes));
        }

        public static void MarkRecvHit64()
        {
            if (!enabled)
            {
                return;
            }

            Interlocked.Increment(ref recvHit64Count);
        }

        public static void Sample(
            int currentWaitSnd,
            int currentPendingPackets,
            long currentPendingPacketBytes,
            long totalDroppedPackets,
            long totalDroppedPacketBytes,
            int currentReassemblyBytes,
            float currentRttLastMs = -1f,
            float currentRttAvgMs = -1f,
            float currentRttRawAvgMs = -1f,
            int currentRttSampleCount = 0,
            int currentRttCongestedSampleCount = 0)
        {
            if (!enabled)
            {
                return;
            }

            waitSnd = currentWaitSnd;
            pendingPackets = currentPendingPackets;
            pendingPacketBytes = currentPendingPacketBytes;
            droppedPackets = totalDroppedPackets;
            droppedPacketBytes = totalDroppedPacketBytes;
            reassemblyBytes = currentReassemblyBytes;
            rttLastMs = currentRttLastMs;
            rttAvgMs = currentRttAvgMs;
            rttRawAvgMs = currentRttRawAvgMs;
            rttSampleCount = currentRttSampleCount;
            rttCongestedSampleCount = currentRttCongestedSampleCount;
            if (currentWaitSnd > peakWaitSnd)
            {
                peakWaitSnd = currentWaitSnd;
            }

            float now = Time.realtimeSinceStartup;
            if (!started)
            {
                started = true;
                lastFlushTime = now;
                return;
            }

            float dt = now - lastFlushTime;
            if (dt < 1f)
            {
                return;
            }

            lastFlushTime = now;
            Flush(dt);
        }

        public static void AppendLine(string line)
        {
            if (!enabled)
            {
                return;
            }

            try
            {
                File.AppendAllText(LogFilePath, $"{DateTime.Now:HH:mm:ss.fff} {line}\n", Encoding.UTF8);
            }
            catch
            {
                // Diagnostics must never affect gameplay.
            }
        }

        public static string GetFullHistory()
        {
            try
            {
                return File.ReadAllText(LogFilePath, Encoding.UTF8);
            }
            catch
            {
                return "(failed to read diagnostics log: " + LogFilePath + ")";
            }
        }

        public static void ResetHistory()
        {
            ResetCounters();
            if (enabled)
            {
                WriteHeader();
                currentSnapshot = "[NetDiag] waiting for samples...";
            }
        }

        private static void Flush(float dt)
        {
            long prodMsgsS = Interlocked.Exchange(ref producedMsgsServer, 0);
            long prodBytesS = Interlocked.Exchange(ref producedBytesServer, 0);
            long prodMsgsC = Interlocked.Exchange(ref producedMsgsClient, 0);
            long prodBytesC = Interlocked.Exchange(ref producedBytesClient, 0);
            long outDg = Interlocked.Exchange(ref outDatagrams, 0);
            long outB = Interlocked.Exchange(ref outBytes, 0);
            long recvDg = Interlocked.Exchange(ref recvDatagrams, 0);
            long recvB = Interlocked.Exchange(ref recvBytes, 0);
            int hit64 = Interlocked.Exchange(ref recvHit64Count, 0);
            int peak = peakWaitSnd;
            peakWaitSnd = waitSnd > 0 ? waitSnd : 0;

            float inv = dt > 0f ? 1f / dt : 1f;
            float prodKbpsS = prodBytesS / 1024f * inv;
            float prodKbpsC = prodBytesC / 1024f * inv;
            float outKbps = outB / 1024f * inv;
            float recvKbps = recvB / 1024f * inv;
            string queueKb = FormatKilobytes(pendingPacketBytes);
            string droppedKb = FormatKilobytes(droppedPacketBytes);
            string reassemblyKb = FormatKilobytes(reassemblyBytes);

            string line =
                $"[NetDiag][IGP] WaitSnd={waitSnd}(peak{peak}) " +
                $"produced S={prodMsgsS}msg/{prodKbpsS.ToString("F1", CultureInfo.InvariantCulture)}KBps " +
                $"C={prodMsgsC}msg/{prodKbpsC.ToString("F1", CultureInfo.InvariantCulture)}KBps " +
                $"out={outDg}pkt/{outKbps.ToString("F1", CultureInfo.InvariantCulture)}KBps " +
                $"recv={recvDg}pkt/{recvKbps.ToString("F1", CultureInfo.InvariantCulture)}KBps " +
                $"hit64={hit64} pktQueue={pendingPackets}/{queueKb}KB " +
                $"pktDrop={droppedPackets}/{droppedKb}KB reassembly={reassemblyKb}KB " +
                $"rtt={FormatMilliseconds(rttLastMs)}/{FormatMilliseconds(rttAvgMs)}ms " +
                $"rawAvg={FormatMilliseconds(rttRawAvgMs)}ms congested={rttCongestedSampleCount}/{rttSampleCount}";

            currentSnapshot =
                $"WaitSnd: {waitSnd}   peak: {peak}\n" +
                $"RTT last/avg: {FormatMilliseconds(rttLastMs)} / {FormatMilliseconds(rttAvgMs)} ms\n" +
                $"RTT rawAvg:   {FormatMilliseconds(rttRawAvgMs)} ms  congested: {rttCongestedSampleCount}/{rttSampleCount}\n" +
                $"produced S: {prodMsgsS}msg  {prodKbpsS.ToString("F1", CultureInfo.InvariantCulture)} KB/s\n" +
                $"produced C: {prodMsgsC}msg  {prodKbpsC.ToString("F1", CultureInfo.InvariantCulture)} KB/s\n" +
                $"KCP out:    {outDg}pkt  {outKbps.ToString("F1", CultureInfo.InvariantCulture)} KB/s\n" +
                $"recv:       {recvDg}pkt  {recvKbps.ToString("F1", CultureInfo.InvariantCulture)} KB/s\n" +
                $"hit64:      {hit64}/s\n" +
                $"pktQueue:   {pendingPackets}  {queueKb} KB\n" +
                $"pktDrop:    {droppedPackets}  {droppedKb} KB\n" +
                $"reassembly: {reassemblyKb} KB";

            Debug.Log(line);
            AppendLine(line);
        }

        private static void ResetCounters()
        {
            Interlocked.Exchange(ref producedMsgsServer, 0);
            Interlocked.Exchange(ref producedBytesServer, 0);
            Interlocked.Exchange(ref producedMsgsClient, 0);
            Interlocked.Exchange(ref producedBytesClient, 0);
            Interlocked.Exchange(ref outDatagrams, 0);
            Interlocked.Exchange(ref outBytes, 0);
            Interlocked.Exchange(ref recvDatagrams, 0);
            Interlocked.Exchange(ref recvBytes, 0);
            Interlocked.Exchange(ref recvHit64Count, 0);
            waitSnd = -1;
            pendingPackets = 0;
            pendingPacketBytes = 0;
            droppedPackets = 0;
            droppedPacketBytes = 0;
            reassemblyBytes = 0;
            peakWaitSnd = 0;
            rttLastMs = -1f;
            rttAvgMs = -1f;
            rttRawAvgMs = -1f;
            rttSampleCount = 0;
            rttCongestedSampleCount = 0;
            started = false;
            lastFlushTime = 0f;
        }

        private static void WriteHeader()
        {
            try
            {
                string path = LogFilePath;
                string? dir = Path.GetDirectoryName(path);
                if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
                {
                    Directory.CreateDirectory(dir);
                }

                File.WriteAllText(path, $"==== [NetDiag] session start {DateTime.Now:yyyy-MM-dd HH:mm:ss} ====\n", Encoding.UTF8);
            }
            catch
            {
                // Diagnostics must never affect gameplay.
            }
        }

        private static string FormatKilobytes(long bytes)
        {
            return (bytes / 1024.0).ToString("F1", CultureInfo.InvariantCulture);
        }

        private static string FormatMilliseconds(float milliseconds)
        {
            if (milliseconds < 0f)
            {
                return "n/a";
            }

            return milliseconds.ToString("F1", CultureInfo.InvariantCulture);
        }
    }
}
