#nullable enable
using System;
using System.Collections.Generic;
using IGP.UnitySDK;
using IGP.UnitySDK.Network;
using Mirror;
using UnityEngine;

namespace IGP.UnitySDK.MirrorTransport
{
    [DisallowMultipleComponent]
    [AddComponentMenu("Network/IGP Mirror Transport")]
    public sealed class IGPMirrorTransport : Transport
    {
        private const uint ConnectRequestMessageType = 41001;
        private const uint ConnectAcceptMessageType = 41002;
        private const uint DisconnectMessageType = 41003;
        private const uint ReliableDataMessageType = 41010;
        private const uint UnreliableDataMessageType = 41011;
        private const int ReliablePacketMaxBytes = 512 * 1024;
        private const int UnreliablePacketMaxBytes = 12 * 1024;
        private const int MaxQueuedMessages = 1024;
        private const int MaxQueuedMessageBytes = 4 * 1024 * 1024;

        [SerializeField] private IGPRuntimeManager? runtimeManager = null;
        [SerializeField] private float connectRetryIntervalSeconds = 0.5f;
        [SerializeField] private float connectTimeoutSeconds = 15f;

        private readonly object queueLock = new object();
        private readonly Queue<QueuedMessage> messageQueue = new Queue<QueuedMessage>();
        private readonly Dictionary<int, string> connectionIdToPlayerId = new Dictionary<int, string>();
        private readonly Dictionary<string, int> playerIdToConnectionId =
            new Dictionary<string, int>(StringComparer.Ordinal);

        private bool runtimeBound;
        private bool serverActive;
        private int nextConnectionId = 1;
        private string? pendingClientAddress;
        private string? pendingClientTargetPlayerId;
        private string? connectedServerPlayerId;
        private int clientConnectionId;
        private float nextConnectAttemptAt;
        private float connectDeadlineAt;
        private int queuedMessageBytes;
        private long droppedQueuedMessages;
        private long droppedQueuedMessageBytes;
        private float nextQueueDropWarningAt;

        private struct QueuedMessage
        {
            public string RemotePlayerId;
            public uint MessageType;
            public byte[] Payload;
        }

        public bool IsClientConnecting => !ClientConnected() && !string.IsNullOrWhiteSpace(pendingClientAddress);
        public string ClientTargetPlayerId => connectedServerPlayerId ?? pendingClientTargetPlayerId ?? string.Empty;
        public int ClientConnectionId => clientConnectionId;
        public int ServerConnectionCount => connectionIdToPlayerId.Count;

        public override bool Available() => true;

        public override bool ClientConnected() => !string.IsNullOrWhiteSpace(connectedServerPlayerId);

        public override void ClientConnect(string address)
        {
            TryBindRuntime();
            pendingClientAddress = string.IsNullOrWhiteSpace(address) ? "host" : address.Trim();
            pendingClientTargetPlayerId = ResolveServerPlayerId(pendingClientAddress);
            connectedServerPlayerId = null;
            clientConnectionId = 0;
            nextConnectAttemptAt = 0f;
            connectDeadlineAt = Time.unscaledTime + connectTimeoutSeconds;
        }

        public override void ClientSend(ArraySegment<byte> segment, int channelId = Channels.Reliable)
        {
            if (!ClientConnected() || string.IsNullOrWhiteSpace(connectedServerPlayerId))
            {
                OnClientError?.Invoke(TransportError.InvalidSend, "IGP client transport is not connected.");
                return;
            }

            var payload = CopySegment(segment);
            IGPNetworkDiagnostics.AddProducedClient(payload.Length);
            if (!TrySendPayload(connectedServerPlayerId!, payload, NormalizeChannel(channelId)))
            {
                OnClientError?.Invoke(TransportError.InvalidSend, "Failed to send Mirror payload over IGP.");
                return;
            }

            OnClientDataSent?.Invoke(new ArraySegment<byte>(payload), NormalizeChannel(channelId));
        }

        public override void ClientDisconnect()
        {
            CompleteClientDisconnect(sendDisconnectPacket: true);
        }

        public override Uri ServerUri()
        {
            return new Uri("igp://host");
        }

        public override bool ServerActive() => serverActive;

        public override void ServerStart()
        {
            TryBindRuntime();
            serverActive = true;
        }

        public override void ServerSend(int connectionId, ArraySegment<byte> segment, int channelId = Channels.Reliable)
        {
            if (!connectionIdToPlayerId.TryGetValue(connectionId, out var remotePlayerId))
            {
                OnServerError?.Invoke(connectionId, TransportError.InvalidSend, "Unknown IGP client connection.");
                return;
            }

            var payload = CopySegment(segment);
            IGPNetworkDiagnostics.AddProducedServer(payload.Length);
            if (!TrySendPayload(remotePlayerId, payload, NormalizeChannel(channelId)))
            {
                OnServerError?.Invoke(connectionId, TransportError.InvalidSend, "Failed to send Mirror payload over IGP.");
                return;
            }

            OnServerDataSent?.Invoke(connectionId, new ArraySegment<byte>(payload), NormalizeChannel(channelId));
        }

        public override void ServerDisconnect(int connectionId)
        {
            if (!connectionIdToPlayerId.TryGetValue(connectionId, out var remotePlayerId))
            {
                return;
            }

            TrySendControl(remotePlayerId, DisconnectMessageType, new byte[] { 1 });
            RemoveServerConnection(connectionId, remotePlayerId, notifyMirror: true);
        }

        public override string ServerGetClientAddress(int connectionId)
        {
            return connectionIdToPlayerId.TryGetValue(connectionId, out var remotePlayerId)
                ? remotePlayerId
                : string.Empty;
        }

        public override void ServerStop()
        {
            var activeConnections = new List<KeyValuePair<int, string>>(connectionIdToPlayerId);
            foreach (var entry in activeConnections)
            {
                TrySendControl(entry.Value, DisconnectMessageType, new byte[] { 1 });
                RemoveServerConnection(entry.Key, entry.Value, notifyMirror: false);
            }

            serverActive = false;
        }

        public override int GetMaxPacketSize(int channelId = Channels.Reliable)
        {
            return NormalizeChannel(channelId) == Channels.Unreliable
                ? UnreliablePacketMaxBytes
                : ReliablePacketMaxBytes;
        }

        public override void ClientEarlyUpdate()
        {
            if (NetworkClient.activeHost)
            {
                return;
            }

            TryBindRuntime();
            ProcessQueuedMessages(forServer: false);
        }

        public override void ServerEarlyUpdate()
        {
            TryBindRuntime();
            ProcessQueuedMessages(forServer: true);
        }

        public override void ClientLateUpdate()
        {
            if (NetworkClient.activeHost)
            {
                return;
            }

            if (ClientConnected() || string.IsNullOrWhiteSpace(pendingClientAddress))
            {
                return;
            }

            pendingClientTargetPlayerId = ResolveServerPlayerId(pendingClientAddress!);
            if (string.IsNullOrWhiteSpace(pendingClientTargetPlayerId))
            {
                return;
            }

            if (Time.unscaledTime >= connectDeadlineAt)
            {
                OnClientError?.Invoke(
                    TransportError.Timeout,
                    $"Timed out connecting to hosted player `{pendingClientTargetPlayerId}`.");
                CompleteClientDisconnect(sendDisconnectPacket: false);
                return;
            }

            if (Time.unscaledTime < nextConnectAttemptAt)
            {
                return;
            }

            if (TrySendControl(pendingClientTargetPlayerId!, ConnectRequestMessageType, new byte[] { 1 }))
            {
                nextConnectAttemptAt = Time.unscaledTime + connectRetryIntervalSeconds;
            }
        }

        public override void Shutdown()
        {
            CompleteClientDisconnect(sendDisconnectPacket: false);
            ServerStop();
            ClearQueuedMessages();
            UnbindRuntime();
        }

        public override void OnApplicationQuit()
        {
            Shutdown();
            base.OnApplicationQuit();
        }

        private void ProcessQueuedMessages(bool forServer)
        {
            if (forServer && !serverActive)
            {
                return;
            }

            List<QueuedMessage>? drained = null;
            lock (queueLock)
            {
                if (messageQueue.Count == 0)
                {
                    return;
                }

                drained = new List<QueuedMessage>(messageQueue.Count);
                while (messageQueue.Count > 0)
                {
                    var item = messageQueue.Dequeue();
                    queuedMessageBytes -= item.Payload?.Length ?? 0;
                    drained.Add(item);
                }
            }

            foreach (var message in drained)
            {
                if (forServer)
                {
                    HandleServerMessage(message);
                }
                else
                {
                    HandleClientMessage(message);
                }
            }
        }

        private void HandleServerMessage(QueuedMessage message)
        {
            if (!TryBindRuntime() || runtimeManager?.Network == null)
            {
                return;
            }

            switch (message.MessageType)
            {
                case ConnectRequestMessageType:
                {
                    runtimeManager.Network.AcceptSessionRequest(runtimeManager.PlayerId, message.RemotePlayerId);
                    var isNewConnection =
                        !playerIdToConnectionId.TryGetValue(message.RemotePlayerId, out var connectionId);
                    if (isNewConnection)
                    {
                        connectionId = nextConnectionId++;
                        playerIdToConnectionId[message.RemotePlayerId] = connectionId;
                        connectionIdToPlayerId[connectionId] = message.RemotePlayerId;

                        NotifyServerConnected(connectionId, message.RemotePlayerId);
                    }

                    TrySendControl(
                        message.RemotePlayerId,
                        ConnectAcceptMessageType,
                        BitConverter.GetBytes(connectionId));
                    break;
                }
                case DisconnectMessageType:
                    if (playerIdToConnectionId.TryGetValue(message.RemotePlayerId, out var disconnectedConnectionId))
                    {
                        RemoveServerConnection(disconnectedConnectionId, message.RemotePlayerId, notifyMirror: true);
                    }

                    break;
                case ReliableDataMessageType:
                case UnreliableDataMessageType:
                    if (!playerIdToConnectionId.TryGetValue(message.RemotePlayerId, out var dataConnectionId))
                    {
                        return;
                    }

                    runtimeManager.Network.AcceptSessionRequest(runtimeManager.PlayerId, message.RemotePlayerId);
                    OnServerDataReceived?.Invoke(
                        dataConnectionId,
                        new ArraySegment<byte>(message.Payload),
                        message.MessageType == UnreliableDataMessageType ? Channels.Unreliable : Channels.Reliable);
                    break;
            }
        }

        private void HandleClientMessage(QueuedMessage message)
        {
            if (!IsMessageFromCurrentServer(message.RemotePlayerId))
            {
                return;
            }

            switch (message.MessageType)
            {
                case ConnectAcceptMessageType:
                    connectedServerPlayerId = message.RemotePlayerId;
                    pendingClientAddress = null;
                    pendingClientTargetPlayerId = null;
                    clientConnectionId = message.Payload.Length >= sizeof(int)
                        ? BitConverter.ToInt32(message.Payload, 0)
                        : 1;

                    if (TryBindRuntime() && runtimeManager?.Network != null)
                    {
                        runtimeManager.Network.AcceptSessionRequest(runtimeManager.PlayerId, message.RemotePlayerId);
                    }

                    OnClientConnected?.Invoke();
                    break;
                case DisconnectMessageType:
                    CompleteClientDisconnect(sendDisconnectPacket: false);
                    break;
                case ReliableDataMessageType:
                case UnreliableDataMessageType:
                    if (!ClientConnected())
                    {
                        return;
                    }

                    OnClientDataReceived?.Invoke(
                        new ArraySegment<byte>(message.Payload),
                        message.MessageType == UnreliableDataMessageType ? Channels.Unreliable : Channels.Reliable);
                    break;
            }
        }

        private bool IsMessageFromCurrentServer(string remotePlayerId)
        {
            if (!string.IsNullOrWhiteSpace(connectedServerPlayerId))
            {
                return string.Equals(connectedServerPlayerId, remotePlayerId, StringComparison.Ordinal);
            }

            if (!string.IsNullOrWhiteSpace(pendingClientTargetPlayerId))
            {
                return string.Equals(pendingClientTargetPlayerId, remotePlayerId, StringComparison.Ordinal);
            }

            return false;
        }

        private void CompleteClientDisconnect(bool sendDisconnectPacket)
        {
            var remotePlayerId = connectedServerPlayerId ?? pendingClientTargetPlayerId;
            var hadState =
                !string.IsNullOrWhiteSpace(remotePlayerId) || !string.IsNullOrWhiteSpace(pendingClientAddress);

            if (sendDisconnectPacket && !string.IsNullOrWhiteSpace(remotePlayerId))
            {
                TrySendControl(remotePlayerId!, DisconnectMessageType, new byte[] { 1 });
            }

            pendingClientAddress = null;
            pendingClientTargetPlayerId = null;
            connectedServerPlayerId = null;
            clientConnectionId = 0;
            nextConnectAttemptAt = 0f;
            connectDeadlineAt = 0f;

            if (hadState)
            {
                OnClientDisconnected?.Invoke();
            }
        }

        private void RemoveServerConnection(int connectionId, string remotePlayerId, bool notifyMirror)
        {
            playerIdToConnectionId.Remove(remotePlayerId);
            connectionIdToPlayerId.Remove(connectionId);

            if (notifyMirror)
            {
                OnServerDisconnected?.Invoke(connectionId);
            }
        }

        private void NotifyServerConnected(int connectionId, string remotePlayerId)
        {
#if IGP_MIRROR_HAS_SERVER_CONNECTED_WITH_ADDRESS
            OnServerConnectedWithAddress?.Invoke(connectionId, remotePlayerId);
#else
#pragma warning disable CS0618
            OnServerConnected?.Invoke(connectionId);
#pragma warning restore CS0618
#endif
        }

        private bool TrySendPayload(string remotePlayerId, byte[] payload, int channelId)
        {
            var messageType = channelId == Channels.Unreliable
                ? UnreliableDataMessageType
                : ReliableDataMessageType;

            return channelId == Channels.Unreliable
                ? TrySendUnreliable(remotePlayerId, messageType, payload)
                : TrySendControl(remotePlayerId, messageType, payload);
        }

        private bool TrySendControl(string remotePlayerId, uint messageType, byte[] payload)
        {
            if (!TryBindRuntime() || runtimeManager?.Network == null || string.IsNullOrWhiteSpace(remotePlayerId))
            {
                return false;
            }

            if (!runtimeManager.IsKcpConnected)
            {
                return false;
            }

            var safePayload = payload.Length == 0 ? new byte[] { 0 } : payload;
            var result = runtimeManager.Network.SendReliableData(
                runtimeManager.PlayerId,
                remotePlayerId,
                safePayload,
                (uint)safePayload.Length,
                messageType);
            return result == IGPNetworkResult.kSuccess;
        }

        private bool TrySendUnreliable(string remotePlayerId, uint messageType, byte[] payload)
        {
            if (!TryBindRuntime() || runtimeManager?.Network == null || string.IsNullOrWhiteSpace(remotePlayerId))
            {
                return false;
            }

            if (!runtimeManager.IsKcpConnected)
            {
                return false;
            }

            var safePayload = payload.Length == 0 ? new byte[] { 0 } : payload;
            var result = runtimeManager.Network.SendData(
                runtimeManager.PlayerId,
                remotePlayerId,
                safePayload,
                (uint)safePayload.Length,
                messageType);
            return result == IGPNetworkResult.kSuccess;
        }

        private bool TryBindRuntime()
        {
            if (runtimeBound && runtimeManager != null && runtimeManager.Network != null)
            {
                return true;
            }

            runtimeManager ??= FindAnyObjectByType<IGPRuntimeManager>();
            if (runtimeManager?.Network == null)
            {
                return false;
            }

            if (!runtimeBound)
            {
                runtimeManager.Network.OnDataReceived.AddListener(HandleNetworkData);
                runtimeBound = true;
            }

            return true;
        }

        private void UnbindRuntime()
        {
            if (!runtimeBound || runtimeManager?.Network == null)
            {
                runtimeBound = false;
                return;
            }

            runtimeManager.Network.OnDataReceived.RemoveListener(HandleNetworkData);
            runtimeBound = false;
        }

        private void HandleNetworkData(IGPDataReceived data)
        {
            if (!IsTransportMessageType(data.message_type))
            {
                return;
            }

            var payload = data.data != null ? (byte[])data.data.Clone() : Array.Empty<byte>();
            lock (queueLock)
            {
                messageQueue.Enqueue(new QueuedMessage
                {
                    RemotePlayerId = data.remote_peer.id,
                    MessageType = data.message_type,
                    Payload = payload,
                });
                queuedMessageBytes += payload.Length;
                TrimQueuedMessages();
            }
        }

        private void TrimQueuedMessages()
        {
            while (messageQueue.Count > 0 &&
                   (messageQueue.Count > MaxQueuedMessages || queuedMessageBytes > MaxQueuedMessageBytes))
            {
                var dropped = messageQueue.Dequeue();
                int droppedBytes = dropped.Payload?.Length ?? 0;
                queuedMessageBytes -= droppedBytes;
                droppedQueuedMessages++;
                droppedQueuedMessageBytes += droppedBytes;
            }

            if (droppedQueuedMessages > 0 && Time.unscaledTime >= nextQueueDropWarningAt)
            {
                Debug.LogWarning(
                    $"[IGP MirrorTransport] Dropped queued messages under backpressure: " +
                    $"dropped={droppedQueuedMessages} droppedKB={(droppedQueuedMessageBytes / 1024.0):F1} " +
                    $"pending={messageQueue.Count} pendingKB={(queuedMessageBytes / 1024.0):F1}");
                droppedQueuedMessages = 0;
                droppedQueuedMessageBytes = 0;
                nextQueueDropWarningAt = Time.unscaledTime + 1f;
            }
        }

        private void ClearQueuedMessages()
        {
            lock (queueLock)
            {
                messageQueue.Clear();
                queuedMessageBytes = 0;
                droppedQueuedMessages = 0;
                droppedQueuedMessageBytes = 0;
                nextQueueDropWarningAt = 0f;
            }
        }

        private static bool IsTransportMessageType(uint messageType)
        {
            return messageType == ConnectRequestMessageType ||
                   messageType == ConnectAcceptMessageType ||
                   messageType == DisconnectMessageType ||
                   messageType == ReliableDataMessageType ||
                   messageType == UnreliableDataMessageType;
        }

        private string ResolveServerPlayerId(string address)
        {
            if (runtimeManager == null)
            {
                return string.Empty;
            }

            if (string.IsNullOrWhiteSpace(address) ||
                string.Equals(address, "host", StringComparison.OrdinalIgnoreCase) ||
                string.Equals(address, "localhost", StringComparison.OrdinalIgnoreCase))
            {
                return runtimeManager.CurrentRoomData.hostId;
            }

            return address.Trim();
        }

        private static int NormalizeChannel(int channelId)
        {
            return channelId == Channels.Unreliable ? Channels.Unreliable : Channels.Reliable;
        }

        private static byte[] CopySegment(ArraySegment<byte> segment)
        {
            var buffer = new byte[segment.Count];
            if (segment.Count > 0 && segment.Array != null)
            {
                Buffer.BlockCopy(segment.Array, segment.Offset, buffer, 0, segment.Count);
            }

            return buffer;
        }
    }
}
