// WingsEmu
// 
// Developed by NosWings Team

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using GameChannel.Cryptography;
using NetCoreServer;
using PhoenixLib.Events;
using PhoenixLib.Logging;
using PhoenixLib.MultiLanguage;
using WingsAPI.Communication.Sessions;
using WingsAPI.Communication.Sessions.Model;
using WingsAPI.Communication.Sessions.Request;
using WingsEmu.Game;
using WingsEmu.Game._i18n;
using WingsEmu.Game._packetHandling;
using WingsEmu.Game.Characters;
using WingsEmu.Game.Characters.Events;
using WingsEmu.Game.Commands;
using WingsEmu.Game.Managers;
using WingsEmu.Game.Maps;
using WingsEmu.Game.Miniland;
using WingsEmu.Game.Networking;
using WingsEmu.Packets;
using WingsEmu.Packets.ClientPackets;

namespace GameChannel.Network
{
    public class GameSession : TcpSession, IClientSession
    {
        private static ISessionManager _sessionManager;
        private static IGlobalCommandExecutor _commandsExecutor;
        private static IAsyncEventPipeline _eventPipeline;
        private static IServerManager _serverManager;
        private static ISessionService _sessionService;
        private static IMinilandManager _minilandManager;
        private static IPacketDeserializer _deserializer;

        private static readonly IPacketSerializer _serializer = new PacketSerializer();


        private static readonly char[] COMMAND_PREFIX = { '$', '%' };
        private static readonly char[] ChatDelimiter = { '/', ':', ';', '!' };


        private readonly IPacketHandlerContainer<ICharacterScreenPacketHandler> _charScreenHandlers;


        private readonly CancellationTokenSource _cts;
        private readonly IPacketHandlerContainer<IGamePacketHandler> _gameHandlers;
        private readonly IGameLanguageService _gameLanguageService;
        private readonly ConcurrentQueue<string> _packetToHandleQueue;
        private readonly ConcurrentQueue<string[]> _pendingPacketsToSend;
        private readonly SemaphoreSlim _semaphoreSlim = new(1, 1);
        private readonly IList<string> _waitForPacketList = new List<string>();

        private readonly char[] PACKET_SPLITTER = { (char)0xFF };

        private bool _isDisconnecting;

        private int _lastKeepAliveIdentity;
        private IPlayerEntity _playerEntity;

        // Packetwait Packets
        private int? _waitForPacketsAmount;

        public GameSession(GameTcpServer server, IPacketHandlerContainer<IGamePacketHandler> gameHandlers,
            IPacketHandlerContainer<ICharacterScreenPacketHandler> charScreenHandlers, IGameLanguageService gameLanguageService) : base(server)
        {
            _gameHandlers = gameHandlers;
            _charScreenHandlers = charScreenHandlers;
            _gameLanguageService = gameLanguageService;
            SessionId = 0;
            _cts = new CancellationTokenSource();
            _pendingPacketsToSend = new ConcurrentQueue<string[]>();
            _packetToHandleQueue = new ConcurrentQueue<string>();
            _ = HandlePacketLoop(_cts.Token);
            _ = SendPacketLoop(_cts.Token);
            Account_OnLangChanged(null, RegionLanguageType.EN);
        }

        public string IpAddress { get; private set; }
        public string HardwareId { get; private set; }
        public string ClientVersion { get; private set; }

        public void SendPackets(IEnumerable<string> packets)
        {
            if (!CanReadOrSend())
            {
                return;
            }

            try
            {
                EnqueuePackets(packets.ToArray());
            }
            catch (Exception e)
            {
                Log.Error("[TCP_SESSION] SendPacket", e);
                ForceDisconnect();
            }
        }

        public void SendPacket(string packet)
        {
            if (!CanReadOrSend())
            {
                return;
            }

            if (string.IsNullOrEmpty(packet))
            {
                return;
            }

            try
            {
                EnqueuePackets(packet);
            }
            catch (Exception e)
            {
                Log.Error("SendPacket", e);
            }
        }


        public RegionLanguageType UserLanguage { get; private set; }
        public Account Account { get; private set; }

        public IPlayerEntity PlayerEntity
        {
            get
            {
                if (_playerEntity == null || !HasSelectedCharacter)
                {
                    Log.Warn("[GAME_SESSION] Uninitialized PlayerEntity cannot be accessed.");
                }

                return _playerEntity;
            }

            private set => _playerEntity = value;
        }


        public IMapInstance CurrentMapInstance { get; set; }

        public bool HasCurrentMapInstance => CurrentMapInstance != null;

        public bool HasSelectedCharacter { get; set; }
        public byte? SelectedCharacterSlot { get; set; }

        public bool IsAuthenticated { get; set; }

        public bool IsDisposing { get; set; }

        public int SessionId { get; set; }

        public bool DebugMode { get; set; }
        public bool GmMode { get; set; } = true;

        public string GetLanguage(string key) => _gameLanguageService.GetLanguage(key, UserLanguage);

        public string GetLanguageFormat(string key, params object[] formatParams) => _gameLanguageService.GetLanguageFormat(key, UserLanguage, formatParams);

        public string GetLanguage(GameDialogKey key) => _gameLanguageService.GetLanguage(key, UserLanguage);

        public string GetLanguageFormat(GameDialogKey key, params object[] formatParams) => _gameLanguageService.GetLanguageFormat(key, UserLanguage, formatParams);

        public static void Initialize(IGlobalCommandExecutor commandExecutor, IAsyncEventPipeline eventPipeline,
            IServerManager serverManager, ISessionManager sessionManager, ISessionService sessionService, IMinilandManager minilandManager, IPacketDeserializer packetDeserializer)
        {
            _sessionManager = sessionManager;
            _commandsExecutor = commandExecutor;
            _eventPipeline = eventPipeline;
            _serverManager = serverManager;
            _sessionService = sessionService;
            _minilandManager = minilandManager;
            _deserializer = packetDeserializer;
        }

        private async Task SendPacketLoop(CancellationToken token)
        {
            while (!token.IsCancellationRequested)
            {
                try
                {
                    FlushPackets();

                    await Task.Delay(50, token);
                }
                catch (TaskCanceledException e)
                {
                    return;
                }
                catch (Exception e)
                {
                    ForceDisconnect();
                    Log.Error("[TCP_SESSION] HandlePacketLoop", e);
                }
            }
        }

        private void FlushPackets()
        {
            try
            {
                if (_pendingPacketsToSend.IsEmpty)
                {
                    return;
                }

                using var stream = new MemoryStream();
                while (_pendingPacketsToSend.TryDequeue(out string[] packets))
                {
                    foreach (string packet in packets)
                    {
                        byte[] bytes = WorldEncrypter.Encrypt(packet, UserLanguage.GetEncoding());
                        stream.Write(bytes);
                    }
                }

                SendAsync(stream.ToArray());
            }
            catch (Exception e)
            {
                Log.Error("[TCP_SESSION] FlushPackets", e);
            }
        }

        private async Task HandlePacketLoop(CancellationToken token)
        {
            while (!token.IsCancellationRequested)
            {
                if (_packetToHandleQueue.IsEmpty)
                {
                    await Task.Delay(50, token);
                    continue;
                }

                if (!_packetToHandleQueue.TryDequeue(out string buff))
                {
                    await Task.Delay(50, token);
                    continue;
                }

                bool isHandling = false;
                try
                {
                    isHandling = await _semaphoreSlim.WaitAsync(0, token);
                    if (isHandling)
                    {
                        OnGamePacketReceived(buff);
                    }
                }
                catch (TaskCanceledException e)
                {
                    return;
                }
                catch (Exception e)
                {
                    ForceDisconnect();
                    Log.Error("[TCP_SESSION] HandlePacketLoop", e);
                }
                finally
                {
                    if (isHandling)
                    {
                        _semaphoreSlim.Release();
                    }
                }

                await Task.Delay(50, token);
            }

            _semaphoreSlim?.Dispose();
        }

        private void EnqueuePackets(string packet)
        {
            _pendingPacketsToSend.Enqueue(new[] { packet });
        }

        private void EnqueuePackets(string[] packets)
        {
            _pendingPacketsToSend.Enqueue(packets);
        }

        private void OnGamePacketReceived(string e)
        {
            try
            {
                HandlePackets(e);
            }
            catch (Exception ex)
            {
                Log.Error("Client_OnPacketReceived : ", ex);
            }
        }

        protected override void OnConnected()
        {
            try
            {
                if (IsDisposed)
                {
                    return;
                }

                if (IsSocketDisposed)
                {
                    ForceDisconnect();
                    return;
                }

                if (Socket == null)
                {
                }
            }
            catch (Exception e)
            {
                Log.Error("[WORLD_SERVER_SESSION] OnConnected", e);
                ForceDisconnect();
            }
        }

        protected override void OnDisconnected()
        {
            ForceDisconnect();
        }

        private bool CanReadOrSend()
        {
            if (IsDisposing)
            {
                return false;
            }

            return !_serverManager.InShutdown;
        }

        protected override void OnReceived(byte[] buffer, long offset, long size)
        {
            if (!CanReadOrSend())
            {
                return;
            }

            try
            {
                string buff = WorldDecrypter.Decrypt(buffer.AsSpan((int)offset, (int)size), SessionId, UserLanguage.GetEncoding());
                var packets = buff.Split(PACKET_SPLITTER, StringSplitOptions.RemoveEmptyEntries).ToList();

                if (HasSelectedCharacter)
                {
                    foreach (string packet in packets)
                    {
                        _packetToHandleQueue.Enqueue(packet);
                    }

                    return;
                }

                foreach (string packet in packets)
                {
                    OnGamePacketReceived(packet);
                }
            }
            catch (Exception e)
            {
                Log.Error("[TCP_SESSION] OnReceived", e);
                ForceDisconnect();
            }
        }

        protected override void OnError(SocketError error)
        {
            Log.Error("[TCP_SESSION]", new Exception($"OnError {error}"));
            ForceDisconnect();
        }

        #region Methods

        public void ForceDisconnect()
        {
            if (_isDisconnecting)
            {
                Log.Debug("[TCP_SESSION] Already disconnecting...");
                return;
            }

            try
            {
                Log.Debug("[TCP_SESSION] Force disconnecting...");
                IsDisposing = true;
                _isDisconnecting = true;
                Log.Debug("[TCP_SESSION] Flushing packets...");
                FlushPackets();
                Log.Debug("[TCP_SESSION] Packets flushed...");
                _pendingPacketsToSend.Clear();
                _packetToHandleQueue.Clear();
                _cts.Cancel();
                if (Account != null)
                {
                    Log.Debug("[TCP_SESSION] Removing Account delegates...");
                    Account.LangChanged -= Account_OnLangChanged;
                }

                // do everything necessary before removing client, DB save, Whatever
                if (!HasSelectedCharacter)
                {
                    Log.Debug("[TCP_SESSION] No character selected...");
                    if (Account != null)
                    {
                        Log.Debug("[TCP_SESSION] Account not null...");
                        _sessionService.Disconnect(new DisconnectSessionRequest
                        {
                            AccountId = Account.Id,
                            EncryptionKey = SessionId
                        }).ConfigureAwait(false).GetAwaiter().GetResult();
                    }

                    Log.Debug($"[TCP_SESSION] {IpAddress} disconnected without selecting character");
                    Disconnect();
                    return;
                }

                Log.Debug("[TCP_SESSION] Clearing buffs...");
                PlayerEntity.BuffComponent.ClearNonPersistentBuffs();
                Log.Debug("[TCP_SESSION] Unregistering session from MapInstance...");
                // CurrentMapInstance?.UnregisterSession(this);
                Log.Debug("[TCP_SESSION] Saving Character...");
                this.CharacterDisconnect().ConfigureAwait(false).GetAwaiter().GetResult();
                Log.Debug("[TCP_SESSION] Unregistering session from SessionManager...");
                _sessionManager.UnregisterSession(this);
                Log.Debug("[TCP_SESSION] Removing session from Master...");
                _minilandManager.RemoveMiniland(PlayerEntity.Id);
                Log.Debug("[TCP_SESSION] Unregistering Miniland...");
                if (Account != null)
                {
                    _sessionService.Disconnect(new DisconnectSessionRequest
                    {
                        AccountId = Account.Id,
                        EncryptionKey = SessionId
                    }).ConfigureAwait(false).GetAwaiter().GetResult();
                }

                Log.Info($"[TCP_SESSION] {PlayerEntity.Name} - {IpAddress} disconnected");
                Disconnect();
            }
            catch (Exception e)
            {
                Log.Error("[TCP_SESSION] Session could not disconnect properly", e);
            }
        }

        public void InitializeAccount(Account account, Session session)
        {
            Account = account;
            IpAddress = session.IpAddress;
            HardwareId = session.HardwareId;
            ClientVersion = session.ClientVersion;
            Account_OnLangChanged(null, account.Language.ToRegionLanguageType());
            Account.LangChanged += Account_OnLangChanged;
            IsAuthenticated = true;
        }

        private void Account_OnLangChanged(object sender, RegionLanguageType e)
        {
            UserLanguage = e;
        }

        //[Obsolete("Primitive string operations will be removed in future, use PacketDefinition SendPacket instead. SendPacket with string parameter should only be used for debugging.")]
        public void SendPacket<T>(T packet) where T : IPacket
        {
            if (IsDisposing)
            {
                return;
            }

            SendPacket(_serializer.Serialize(packet));
        }


        public void InitializePlayerEntity(IPlayerEntity character)
        {
            HasSelectedCharacter = true;
            PlayerEntity = character;
            PlayerEntity.SetSession(this);
            _sessionManager.RegisterSession(this);
        }

        public void EmitEvent<T>(T e) where T : PlayerEvent
        {
            EmitEventAsync(e).ConfigureAwait(false).GetAwaiter().GetResult();
        }

        public async Task EmitEventAsync<T>(T e) where T : PlayerEvent
        {
            e.Sender = this;
            await _eventPipeline.ProcessEventAsync(e).ConfigureAwait(false);
        }

        private void ProcessUnAuthedPacket(string sessionPacket)
        {
            if (string.IsNullOrEmpty(sessionPacket))
            {
                return;
            }

            string[] sessionParts = sessionPacket.Split(' ');
            if (sessionParts.Length == 0)
            {
                return;
            }

            if (!int.TryParse(sessionParts[0], out int packetId))
            {
                ForceDisconnect();
                return;
            }

            _lastKeepAliveIdentity = packetId;

            // set the SessionId if Session Packet arrives
            if (sessionParts.Length < 2)
            {
                return;
            }

            if (!int.TryParse(sessionParts[1].Split('\\').FirstOrDefault(), out int sessionId))
            {
                return;
            }

            SessionId = sessionId;

            if (_waitForPacketsAmount.HasValue)
            {
                return;
            }

            _waitForPacketsAmount = 3;
            _waitForPacketList.Add(EntryPointPacket.EntryPointPacketHeader);
        }

        /// <summary>
        ///     Handle the packet received by the Client.
        /// </summary>
        private void HandlePackets(string packet)
        {
            // determine first packet
            if (SessionId == 0)
            {
                ProcessUnAuthedPacket(packet);
                return;
            }

            string packetString = packet.Replace('^', ' ');
            string[] packetSplit = packetString.Split(' ');

            if (_waitForPacketsAmount.HasValue)
            {
                WaitForEntrypointPackets(packetSplit);
                return;
            }

            ProcessWorldPackets(packetSplit, packetString, packet);
        }

        private void ProcessWorldPackets(IList<string> packetSplit, string packetString, string packet)
        {
            // keep alive
            string nextKeepAliveRaw = packetSplit[0];
            if (!int.TryParse(nextKeepAliveRaw, out int nextKeepaliveIdentity) && nextKeepaliveIdentity != (_lastKeepAliveIdentity + 1))
            {
                Log.Warn("CORRUPTED_KEEPALIVE " + IpAddress);
                ForceDisconnect();
                return;
            }

            if (nextKeepaliveIdentity == 0)
            {
                if (_lastKeepAliveIdentity == ushort.MaxValue)
                {
                    _lastKeepAliveIdentity = nextKeepaliveIdentity;
                }
            }
            else
            {
                _lastKeepAliveIdentity = nextKeepaliveIdentity;
            }


            if (packetSplit.Count <= 1)
            {
                return;
            }

            if (packetSplit[1].Length < 1)
            {
                return;
            }

            if (COMMAND_PREFIX.Any(s => s == packetSplit[1][0]))
            {
                _commandsExecutor.HandleCommand(packet.Substring(packet.IndexOf(' ', StringComparison.OrdinalIgnoreCase) + 1), this, packetSplit[1][0].ToString());
                return;
            }

            if (packetSplit[1].Length >= 1 && ChatDelimiter.Any(s => s == packetSplit[1][0]))
            {
                packetSplit[1] = packetSplit[1][0].ToString();
                packetString = packet.Insert(packet.IndexOf(' ', StringComparison.OrdinalIgnoreCase) + 2, " ");
            }

            if (packetSplit[1] != "0")
            {
                TriggerHandler(packetSplit[1].Replace("#", "", StringComparison.OrdinalIgnoreCase), packetString);
            }
        }

        private void WaitForEntrypointPackets(IReadOnlyList<string> packetSplit)
        {
            if (packetSplit.Count < 2)
            {
                return;
            }

            // cross server authentication
            if (packetSplit.Count > 3 && packetSplit[1] == "DAC")
            {
                _waitForPacketList.Clear();
                _waitForPacketList.Add(string.Join(" ", packetSplit.Skip(1).ToArray()));
                _waitForPacketsAmount = 1;
            }
            else
            {
                // username or password
                _waitForPacketList.Add(packetSplit[1]);
            }

            if (_waitForPacketList.Count != _waitForPacketsAmount)
            {
                return; // continue;
            }

            _waitForPacketsAmount = null;
            string queuedPackets = string.Join(" ", _waitForPacketList.ToArray());
            string header = queuedPackets.Split(' ', '^')[0];
            TriggerHandler(header, queuedPackets);
            _waitForPacketList.Clear();
        }

        private void TriggerHandler(string packetHeader, string packetString)
        {
            if (_serverManager.InShutdown)
            {
                return;
            }

            if (IsDisposing)
            {
                Log.Warn("[CLIENTSESSION] DISPOSING");
                return;
            }

            try
            {
                (IClientPacket typedPacket, Type packetType) = _deserializer.Deserialize(packetString, IsAuthenticated);

                if (packetType == typeof(UnresolvedPacket) && typedPacket != null)
                {
                    Log.Warn($"AccountId: {(Account?.Id ?? 0).ToString()} UNRESOLVED_PACKET : {packetHeader}");
                    return;
                }

                if (packetType == null && typedPacket == null)
                {
                    Log.Info("DESERIALIZATION_ERROR");
                    return;
                }

                if (HasSelectedCharacter)
                {
                    _gameHandlers.Execute(this, typedPacket, packetType);
                }
                else
                {
                    _charScreenHandlers.Execute(this, typedPacket, packetType);
                }
            }
            catch (Exception ex)
            {
                Log.Error("[Handler Error]", ex);
            }
        }

        #endregion
    }
}