#nullable enable
using Barotrauma.Steam;
using System;
using System.Collections.Generic;
using System.Threading;
using Barotrauma.Extensions;

namespace Barotrauma.Networking
{
    sealed class SteamP2POwnerPeer : ClientPeer
    {
        private readonly SteamId selfSteamID;
        private UInt64 ownerKey64 => unchecked((UInt64)ownerKey.Fallback(0));

        private SteamId ReadSteamId(IReadMessage inc) => new SteamId(inc.ReadUInt64() ^ ownerKey64);
        private void WriteSteamId(IWriteMessage msg, SteamId val) => msg.WriteUInt64(val.Value ^ ownerKey64);

        private long sentBytes, receivedBytes;

        private sealed class RemotePeer
        {
            public readonly SteamId SteamId;
            public Option<SteamId> OwnerSteamId;
            public double? DisconnectTime;
            public bool Authenticating;
            public bool Authenticated;

            public readonly struct UnauthedMessage
            {
                public readonly SteamId Sender;
                public readonly byte[] Bytes;
                public readonly int Length;

                public UnauthedMessage(SteamId sender, byte[] bytes)
                {
                    Sender = sender;
                    Bytes = bytes;
                    Length = bytes.Length;
                }
            }

            public readonly List<UnauthedMessage> UnauthedMessages;

            public RemotePeer(SteamId steamId)
            {
                SteamId = steamId;
                OwnerSteamId = Option<SteamId>.None();
                DisconnectTime = null;
                Authenticating = false;
                Authenticated = false;

                UnauthedMessages = new List<UnauthedMessage>();
            }
        }

        private List<RemotePeer> remotePeers = null!;

        public SteamP2POwnerPeer(Callbacks callbacks, int ownerKey) : base(new PipeEndpoint(), callbacks, Option<int>.Some(ownerKey))
        {
            ServerConnection = null;

            isActive = false;

            selfSteamID = SteamManager.GetSteamId().TryUnwrap(out var steamId)
                ? steamId
                : throw new InvalidOperationException("Steamworks not initialized");
        }


        public override void Start()
        {
            if (isActive) { return; }

            initializationStep = ConnectionInitialization.SteamTicketAndVersion;

            ServerConnection = new PipeConnection(selfSteamID)
            {
                Status = NetworkConnectionStatus.Connected
            };

            remotePeers = new List<RemotePeer>();

            Steamworks.SteamNetworking.ResetActions();
            Steamworks.SteamNetworking.OnP2PSessionRequest = OnIncomingConnection;
            Steamworks.SteamUser.OnValidateAuthTicketResponse += OnAuthChange;

            Steamworks.SteamNetworking.AllowP2PPacketRelay(true);

            isActive = true;
        }

        private void OnAuthChange(Steamworks.SteamId steamId, Steamworks.SteamId ownerId, Steamworks.AuthResponse status)
        {
            RemotePeer? remotePeer = remotePeers.Find(p => p.SteamId.Value == steamId);

            if (remotePeer == null) { return; }

            if (status == Steamworks.AuthResponse.OK)
            {
                if (remotePeer.Authenticated) { return; }

                SteamId ownerSteamId = new SteamId(ownerId);
                remotePeer.OwnerSteamId = Option<SteamId>.Some(ownerSteamId);
                remotePeer.Authenticated = true;
                remotePeer.Authenticating = false;
                foreach (var unauthedMessage in remotePeer.UnauthedMessages)
                {
                    IWriteMessage msg = new WriteOnlyMessage();
                    WriteSteamId(msg, unauthedMessage.Sender);
                    WriteSteamId(msg, ownerSteamId);
                    msg.WriteBytes(unauthedMessage.Bytes, 0, unauthedMessage.Length);
                    ForwardToServerProcess(msg);
                }

                remotePeer.UnauthedMessages.Clear();
            }
            else
            {
                DisconnectPeer(remotePeer, PeerDisconnectPacket.SteamAuthError(status));
            }
        }

        private void OnIncomingConnection(Steamworks.SteamId steamId)
        {
            if (!isActive) { return; }

            if (remotePeers.None(p => p.SteamId.Value == steamId))
            {
                remotePeers.Add(new RemotePeer(new SteamId(steamId)));
            }

            Steamworks.SteamNetworking.AcceptP2PSessionWithUser(steamId); //accept all connections, the server will figure things out later
        }

        private void OnP2PData(ulong steamId, IReadMessage inc)
        {
            if (!isActive) { return; }

            RemotePeer? remotePeer = remotePeers.Find(p => p.SteamId.Value == steamId);
            if (remotePeer == null) { return; }

            if (remotePeer.DisconnectTime != null) { return; }

            try
            {
                ProcessP2PData(steamId, remotePeer, inc);
            }
            catch (Exception e)
            {
                string errorMsg = $"Server failed to read an incoming P2P message. {{{e}}}\n{e.StackTrace.CleanupStackTrace()}";
                GameAnalyticsManager.AddErrorEventOnce($"SteamP2POwnerPeer.OnP2PData:OwnerReadException{e.TargetSite}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
#if DEBUG
                DebugConsole.ThrowError(errorMsg);
#else
                if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError(errorMsg); }
#endif
            }
        }

        private void ProcessP2PData(ulong steamId, RemotePeer remotePeer, IReadMessage inc)
        {
            var (deliveryMethod, packetHeader, connectionInitialization) = INetSerializableStruct.Read<PeerPacketHeaders>(inc);

            if (remotePeer is { Authenticated: false, Authenticating: false } && packetHeader.IsConnectionInitializationStep())
            {
                remotePeer.DisconnectTime = null;

                ConnectionInitialization initialization = connectionInitialization ?? throw new Exception("Initialization step missing");
                if (initialization == ConnectionInitialization.SteamTicketAndVersion)
                {
                    remotePeer.Authenticating = true;

                    var packet = INetSerializableStruct.Read<ClientSteamTicketAndVersionPacket>(inc);

                    packet.SteamAuthTicket.TryUnwrap(out var ticket);

                    Steamworks.BeginAuthResult authSessionStartState = SteamManager.StartAuthSession(ticket, steamId);
                    if (authSessionStartState != Steamworks.BeginAuthResult.OK)
                    {
                        DisconnectPeer(remotePeer, PeerDisconnectPacket.SteamAuthError(authSessionStartState));
                        return;
                    }
                }
            }

            var steamUserId = new SteamId(steamId);
            if (remotePeer.Authenticating)
            {
                remotePeer.UnauthedMessages.Add(new RemotePeer.UnauthedMessage(steamUserId, inc.Buffer));
            }
            else
            {
                IWriteMessage outMsg = new WriteOnlyMessage();
                WriteSteamId(outMsg, steamUserId);
                WriteSteamId(outMsg, remotePeer.OwnerSteamId.Fallback(steamUserId));
                outMsg.WriteBytes(inc.Buffer, 0, inc.LengthBytes);

                ForwardToServerProcess(outMsg);
            }

        }

        public override void Update(float deltaTime)
        {
            if (!isActive) { return; }

            if (ChildServerRelay.HasShutDown || !ChildServerRelay.IsProcessAlive)
            {
                var gameClient = GameMain.Client;
                Close(PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed));
                gameClient?.CreateServerCrashMessage();
                return;
            }

            for (int i = remotePeers.Count - 1; i >= 0; i--)
            {
                if (remotePeers[i].DisconnectTime != null && remotePeers[i].DisconnectTime < Timing.TotalTime)
                {
                    ClosePeerSession(remotePeers[i]);
                }
            }

            for (int i = 0; i < 100; i++)
            {
                if (!Steamworks.SteamNetworking.IsP2PPacketAvailable()) { break; }

                var packet = Steamworks.SteamNetworking.ReadP2PPacket();
                if (packet is { SteamId: var steamId, Data: var data })
                {
                    OnP2PData(steamId, new ReadWriteMessage(data, 0, data.Length * 8, false));
                    receivedBytes += data.Length;
                }
            }

            GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.ReceivedBytes, receivedBytes);
            GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.SentBytes, sentBytes);

            while (ChildServerRelay.Read(out byte[] incBuf))
            {
                ChildServerRelay.DisposeLocalHandles();
                IReadMessage inc = new ReadOnlyMessage(incBuf, false, 0, incBuf.Length, ServerConnection);
                HandleDataMessage(inc);
            }
        }

        private void HandleDataMessage(IReadMessage inc)
        {
            if (!isActive) { return; }

            SteamId recipientSteamId = ReadSteamId(inc);

            var peerPacketHeaders = INetSerializableStruct.Read<PeerPacketHeaders>(inc);

            if (recipientSteamId != selfSteamID)
            {
                HandleMessageForRemotePeer(peerPacketHeaders, recipientSteamId, inc);
            }
            else
            {
                HandleMessageForOwner(peerPacketHeaders, inc);
            }
        }

        private static byte[] GetRemainingBytes(IReadMessage msg)
        {
            return msg.Buffer[msg.BytePosition..msg.LengthBytes];
        }
        
        private void HandleMessageForRemotePeer(PeerPacketHeaders peerPacketHeaders, SteamId recipientSteamId, IReadMessage inc)
        {
            var (deliveryMethod, packetHeader, initialization) = peerPacketHeaders;
            
            if (!packetHeader.IsServerMessage())
            {
                DebugConsole.ThrowError("Received non-server message meant for remote peer");
                return;
            }

            RemotePeer? peer = remotePeers.Find(p => p.SteamId == recipientSteamId);
            if (peer is null) { return; }

            if (packetHeader.IsDisconnectMessage())
            {
                var packet = INetSerializableStruct.Read<PeerDisconnectPacket>(inc);
                DisconnectPeer(peer, packet);
                return;
            }

            IWriteMessage outMsg = new WriteOnlyMessage();

            outMsg.WriteNetSerializableStruct(new PeerPacketHeaders
            {
                DeliveryMethod = deliveryMethod,
                PacketHeader = packetHeader,
                Initialization = initialization
            });

            if (packetHeader.IsConnectionInitializationStep())
            {
                var initRelayPacket = new SteamP2PInitializationRelayPacket
                {
                    LobbyID = SteamManager.CurrentLobbyID,
                    Message = new PeerPacketMessage
                    {
                        Buffer = GetRemainingBytes(inc)
                    }
                };

                outMsg.WriteNetSerializableStruct(initRelayPacket);
            }
            else
            {
                byte[] userMessage = GetRemainingBytes(inc);
                outMsg.WriteBytes(userMessage, 0, userMessage.Length);
            }

            ForwardToRemotePeer(deliveryMethod, recipientSteamId, outMsg);
        }

        private void HandleMessageForOwner(PeerPacketHeaders peerPacketHeaders, IReadMessage inc)
        {
            var (_, packetHeader, _) = peerPacketHeaders;

            if (packetHeader.IsDisconnectMessage())
            {
                DebugConsole.ThrowError("Received disconnect message from owned server");
                return;
            }

            if (!packetHeader.IsServerMessage())
            {
                DebugConsole.ThrowError("Received non-server message from owned server");
                return;
            }

            if (packetHeader.IsHeartbeatMessage())
            {
                return; //no timeout since we're using pipes, ignore this message
            }

            if (packetHeader.IsConnectionInitializationStep())
            {
                IWriteMessage outMsg = new WriteOnlyMessage();
                WriteSteamId(outMsg, selfSteamID);
                WriteSteamId(outMsg, selfSteamID);
                outMsg.WriteNetSerializableStruct(new PeerPacketHeaders
                {
                    DeliveryMethod = DeliveryMethod.Reliable,
                    PacketHeader = PacketHeader.IsConnectionInitializationStep,
                    Initialization = ConnectionInitialization.SteamTicketAndVersion
                });
                outMsg.WriteNetSerializableStruct(new SteamP2PInitializationOwnerPacket
                {
                    OwnerName = GameMain.Client.Name
                });
                ForwardToServerProcess(outMsg);
            }
            else
            {
                if (initializationStep != ConnectionInitialization.Success)
                {
                    callbacks.OnInitializationComplete.Invoke();
                    initializationStep = ConnectionInitialization.Success;
                }

                PeerPacketMessage packet = INetSerializableStruct.Read<PeerPacketMessage>(inc);
                IReadMessage msg = new ReadOnlyMessage(packet.Buffer, packetHeader.IsCompressed(), 0, packet.Length, ServerConnection);
                callbacks.OnMessageReceived.Invoke(msg);
            }
        }
        
        private void DisconnectPeer(RemotePeer peer, PeerDisconnectPacket peerDisconnectPacket)
        {
            peer.DisconnectTime ??= Timing.TotalTime + 1.0;

            IWriteMessage outMsg = new WriteOnlyMessage();
            outMsg.WriteNetSerializableStruct(new PeerPacketHeaders
            {
                DeliveryMethod = DeliveryMethod.Reliable,
                PacketHeader = PacketHeader.IsServerMessage | PacketHeader.IsDisconnectMessage
            });
            outMsg.WriteNetSerializableStruct(peerDisconnectPacket);
            
            Steamworks.SteamNetworking.SendP2PPacket(peer.SteamId.Value, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable);
            sentBytes += outMsg.LengthBytes;
        }

        private void ClosePeerSession(RemotePeer peer)
        {
            Steamworks.SteamNetworking.CloseP2PSessionWithUser(peer.SteamId.Value);
            remotePeers.Remove(peer);
        }

        public override void SendPassword(string password)
        {
            //owner doesn't send passwords
        }

        public override void Close(PeerDisconnectPacket peerDisconnectPacket)
        {
            if (!isActive) { return; }

            isActive = false;

            for (int i = remotePeers.Count - 1; i >= 0; i--)
            {
                DisconnectPeer(remotePeers[i], PeerDisconnectPacket.WithReason(DisconnectReason.ServerShutdown));
            }

            Thread.Sleep(100);

            for (int i = remotePeers.Count - 1; i >= 0; i--)
            {
                ClosePeerSession(remotePeers[i]);
            }

            callbacks.OnDisconnect.Invoke(peerDisconnectPacket);

            SteamManager.LeaveLobby();
            Steamworks.SteamNetworking.ResetActions();
            Steamworks.SteamUser.OnValidateAuthTicketResponse -= OnAuthChange;
        }

        public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod, bool compressPastThreshold = true)
        {
            if (!isActive) { return; }

            IWriteMessage msgToSend = new WriteOnlyMessage();
            byte[] msgData = msg.PrepareForSending(compressPastThreshold, out bool isCompressed, out _);
            WriteSteamId(msgToSend, selfSteamID);
            WriteSteamId(msgToSend, selfSteamID);
            msgToSend.WriteNetSerializableStruct(new PeerPacketHeaders
            {
                DeliveryMethod = deliveryMethod,
                PacketHeader = isCompressed ? PacketHeader.IsCompressed : PacketHeader.None
            });
            msgToSend.WriteNetSerializableStruct(new PeerPacketMessage
            {
                Buffer = msgData
            });
            ForwardToServerProcess(msgToSend);
        }

        protected override void SendMsgInternal(PeerPacketHeaders headers, INetSerializableStruct? body)
        {
            //not currently used by SteamP2POwnerPeer
            throw new NotImplementedException();
        }

        private static void ForwardToServerProcess(IWriteMessage msg)
        {
            byte[] bufToSend = new byte[msg.LengthBytes];
            msg.Buffer[..msg.LengthBytes].CopyTo(bufToSend.AsSpan());
            ChildServerRelay.Write(bufToSend);
        }

        private void ForwardToRemotePeer(DeliveryMethod deliveryMethod, SteamId recipent, IWriteMessage outMsg)
        {
            byte[] buf = outMsg.PrepareForSending(compressPastThreshold: false, out _, out int length);

            if (length + 4 >= MsgConstants.MTU)
            {
                DebugConsole.Log($"WARNING: message length comes close to exceeding MTU, forcing reliable send ({length} bytes)");
                deliveryMethod = DeliveryMethod.Reliable;
            }

            bool successSend = Steamworks.SteamNetworking.SendP2PPacket(recipent.Value, buf, length, 0, deliveryMethod.ToSteam());
            sentBytes += length;

            if (successSend) { return; }

            if (deliveryMethod is DeliveryMethod.Unreliable)
            {
                DebugConsole.Log($"WARNING: message couldn't be sent unreliably, forcing reliable send ({length} bytes)");
                successSend = Steamworks.SteamNetworking.SendP2PPacket(recipent.Value, buf, length, 0, DeliveryMethod.Reliable.ToSteam());
                sentBytes += length;
            }

            if (!successSend)
            {
                DebugConsole.AddWarning($"Failed to send message to remote peer! ({length} bytes)");
            }
        }

#if DEBUG
        public override void ForceTimeOut()
        {
            //TODO: reimplement?
        }
#endif
    }
}