using System.Net;
using System.Net.WebSockets;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Unicode;
using KfChatDotNetWsClient.Models;
using KfChatDotNetWsClient.Models.Events;
using KfChatDotNetWsClient.Models.Json;
using NLog;
using Websocket.Client;
// It's a fucking lie. You must use conditional access or you WILL get NullReferenceErrors if an event is not in use
// ReSharper disable ConditionalAccessQualifierIsNonNullableAccordingToAPIContract

namespace KfChatDotNetWsClient;

public class ChatClient
{
    public event EventHandlers.OnMessagesEventHandler? OnMessages;
    public event EventHandlers.OnUsersPartedEventHandler? OnUsersParted;
    public event EventHandlers.OnUsersJoinedEventHandler? OnUsersJoined;
    public event EventHandlers.OnWsReconnectEventHandler? OnWsReconnect;
    public event EventHandlers.OnDeleteMessagesEventHandler? OnDeleteMessages;
    public event EventHandlers.OnWsDisconnectionEventHandler? OnWsDisconnection;
    public event EventHandlers.OnFailedToJoinRoom? OnFailedToJoinRoom;
    public event EventHandlers.OnUnknownCommand? OnUnknownCommand;
    public event EventHandlers.OnPermissionsEventHandler? OnPermissions;
    public event EventHandlers.OnSystemMessage? OnSystemMessage;
    public event EventHandlers.OnMotdEventHandler? OnMotd;
    public event EventHandlers.OnWhisperEventHandler? OnWhisper;
    private WebsocketClient? _wsClient;
    private readonly Logger _logger = LogManager.GetCurrentClassLogger();
    private ChatClientConfigModel _config;
    public DateTime LastPacketReceived = DateTime.UtcNow;

    public ChatClient(ChatClientConfigModel config)
    {
        _config = config;
    }

    public void UpdateConfig(ChatClientConfigModel config)
    {
        _config = config;
    }

    public void UpdateCookies(Dictionary<string, string> cookies)
    {
        _config.Cookies = cookies;
    }

    public async Task StartWsClient()
    {
        await CreateWsClient();
    }

    public void Disconnect()
    {
        if (_wsClient == null) throw new WebSocketNotInitializedException();
        _wsClient.Stop(WebSocketCloseStatus.NormalClosure, "Closing websocket").Wait();
    }

    // Bot is inconsistent with what methods are async and not but I don't want to change the Disconnect() method to be
    // async as then it'll fuck up anyone not waiting for it. This is why there's an explicit async method for this but
    // none for reconnect.
    public async Task DisconnectAsync()
    {
        if (_wsClient == null) throw new WebSocketNotInitializedException();
        await _wsClient.Stop(WebSocketCloseStatus.NormalClosure, "Closing websocket");
    }

    public async Task ReconnectAsync()
    {
        if (_wsClient == null) throw new WebSocketNotInitializedException();
        await _wsClient.Reconnect();
    }

    private async Task CreateWsClient()
    {
        var factory = new Func<ClientWebSocket>(() =>
        {
            var clientWs = new ClientWebSocket();
            if (_config.Proxy != null)
            {
                clientWs.Options.Proxy = new WebProxy(_config.Proxy);
            }
            // Guest mode
            if (_config.Cookies.Keys.Count == 0)
            {
                return clientWs;
            }

            var cookieContainer = new CookieContainer();
            foreach (var key in _config.Cookies.Keys)
            {
                cookieContainer.Add(new Cookie(key, _config.Cookies[key], "/", _config.CookieDomain));
            }
            clientWs.Options.Cookies = cookieContainer;

            return clientWs;
        });

        var client = new WebsocketClient(_config.WsUri, factory)
        {
            IsReconnectionEnabled = false
        };
        _wsClient = client;

        client.ReconnectionHappened.Subscribe(WsReconnection);
        client.MessageReceived.Subscribe(WsMessageReceived);
        client.DisconnectionHappened.Subscribe(WsDisconnection);

        _logger.Debug("Websocket client has been built, about to start");
        await client.Start();
        _logger.Debug("Websocket client started!");
    }

    public void DisposeWsClient()
    {
        if (_wsClient == null) throw new WebSocketNotInitializedException();
        _wsClient.Dispose();
    }

    public bool IsConnected()
    {
        return _wsClient is { IsRunning: true };
    }

    private void WsDisconnection(DisconnectionInfo disconnectionInfo)
    {
        _logger.Error($"Client disconnected from the chat (or never successfully connected). Type is {disconnectionInfo.Type}");
        _logger.Error($"Close Status => {disconnectionInfo.CloseStatus}; Close Status Description => {disconnectionInfo.CloseStatusDescription}");
        _logger.Error(disconnectionInfo.Exception);
        OnWsDisconnection?.Invoke(this, disconnectionInfo);
    }

    private void WsReconnection(ReconnectionInfo reconnectionInfo)
    {
        _logger.Error($"Websocket connection dropped and reconnected. Reconnection type is {reconnectionInfo.Type}");
        OnWsReconnect?.Invoke(this, reconnectionInfo);
    }

    private void WsMessageReceived(ResponseMessage message)
    {
        LastPacketReceived = DateTime.UtcNow;
        if (message.Text == null)
        {
            _logger.Info("Websocket message was null, ignoring packet");
            return;
        }

        if (message.Text.StartsWith("You cannot join this room"))
        {
            _logger.Debug("Got a message saying we failed to join the room");
            OnFailedToJoinRoom?.Invoke(this, message.Text);
            return;
        }

        if (message.Text.StartsWith("Unknown command"))
        {
            _logger.Debug("Unknown command received");
            OnUnknownCommand?.Invoke(this, message.Text);
            return;
        }

        Dictionary<string, object> packetType;
        try
        {
            packetType = JsonSerializer.Deserialize<Dictionary<string, object>>(message.Text)!;
        }
        catch (Exception e)
        {
            _logger.Error("Failed to parse packet");
            _logger.Error(e);
            _logger.Error($"Packet contents: {message.Text}");
            return;
        }

        _logger.Debug($"Received packet from KF: {string.Join(',', packetType.Keys)}");
        // Message(s) received
        if (packetType.ContainsKey("messages"))
        {
            _logger.Debug("Looks like it's a chat message");
            WsChatMessagesReceived(message);
            return;
        }
        // User(s) joined
        if (packetType.ContainsKey("users"))
        {
            _logger.Debug("Looks like this is a user(s) joined packet");
            WsChatUsersJoined(message);
            return;
        }
        // User(s) parted
        if (packetType.ContainsKey("user"))
        {
            _logger.Debug("Looks like this is a user(s) parted packet");
            WsChatUsersParted(message);
            return;
        }

        if (packetType.ContainsKey("permissions"))
        {
            _logger.Debug("Looks like we got permissions");
            WsPermissions(message);
            return;
        }

        if (packetType.ContainsKey("system"))
        {
            _logger.Debug("Looks like a system message");
            WsSystemMessage(message);
            return;
        }

        if (packetType.ContainsKey("delete"))
        {
            _logger.Debug($"Looks like this is a message deletion packet");
            WsDeleteMessagesReceived(message);
            return;
        }

        if (packetType.ContainsKey("whisper"))
        {
            _logger.Debug("Looks like this is a whisper packet");
            WsWhisper(message);
            return;
        }

        if (packetType.ContainsKey("motd"))
        {
            _logger.Debug("Looks like this is an MOTD packet");
            WsMotd(message);
            return;
        }
        
        _logger.Info($"Received packet this was not handled: {message.Text}");
    }

    public void JoinRoom(int roomId)
    {
        _logger.Debug($"Joining {roomId}");
        if (_wsClient == null) throw new WebSocketNotInitializedException();
        _wsClient.Send($"/join {roomId}");
    }
    
    public void SendMessage(string message)
    {
        _logger.Debug($"Sending '{message}'");
        if (_wsClient == null) throw new WebSocketNotInitializedException();
        _wsClient.Send(message);
    }

    public async Task SendMessageInstantAsync(string message)
    {
        _logger.Debug($"Sending '{message}', bypassing the queue");
        if (_wsClient == null) throw new WebSocketNotInitializedException();
        await _wsClient.SendInstant(message);
    }

    public void DeleteMessage(int messageId)
    {
        _logger.Debug($"Deleting {messageId}");
        if (_wsClient == null) throw new WebSocketNotInitializedException();
        _wsClient.Send($"/delete {messageId}");
    }

    public async Task DeleteMessageAsync(string messageUuid)
    {
        _logger.Debug($"Deleting {messageUuid}");
        if (_wsClient == null) throw new WebSocketNotInitializedException();
        await _wsClient.SendInstant($"/delete {messageUuid}");
    }
    
    public void EditMessage(string messageUuid, string newMessage)
    {
        var payload = JsonSerializer.Serialize(new EditMessageJsonModel {Uuid = messageUuid, Message = newMessage});
        _logger.Debug($"Editing {messageUuid} with '{newMessage}'");
        if (_wsClient == null) throw new WebSocketNotInitializedException();
        _wsClient.Send($"/edit {payload}");
    }

    public async Task EditMessageAsync(string messageUuid, string newMessage)
    {
        var settings = new TextEncoderSettings();
        settings.AllowRange(UnicodeRanges.All);
        var options = new JsonSerializerOptions()
        {
            Encoder = JavaScriptEncoder.Create(settings)
        };
        var payload = JsonSerializer.Serialize(new EditMessageJsonModel {Uuid = messageUuid, Message = newMessage}, options);
        _logger.Debug($"Editing {messageUuid} with '{newMessage}'");
        if (_wsClient == null) throw new WebSocketNotInitializedException();
        var msg = $"/edit {payload}";
        var length = Encoding.UTF8.GetByteCount(msg);
        if (length > 2048)
        {
            _logger.Error($"Edit message is too long at {length} bytes");
        }
        await _wsClient.SendInstant($"/edit {payload}");
    }

    public async Task SetMotd(string messageUuid)
    {
        _logger.Debug($"Setting {messageUuid} as the MOTD");
        if (_wsClient == null) throw new WebSocketNotInitializedException();
        await _wsClient.SendInstant($"/motd {messageUuid}");
    }

    private void WsDeleteMessagesReceived(ResponseMessage message)
    {
        var data = JsonSerializer.Deserialize<DeleteMessagesJsonModel>(message.Text!);
        _logger.Debug($"Received delete packet for messages: {string.Join(',', data!.MessageIdsToDelete)}");
        OnDeleteMessages?.Invoke(this, data.MessageIdsToDelete);
    }

    private void WsChatMessagesReceived(ResponseMessage message)
    {
        var data = JsonSerializer.Deserialize<MessagesJsonModel>(message.Text!);
        var messages = new List<MessageModel>();
        foreach (var chatMessage in data!.Messages)
        {
            var model = new MessageModel
            {
                Author = new UserModel
                {
                    Id = chatMessage.Author.Id,
                    Username = chatMessage.Author.Username,
                    AvatarUrl = chatMessage.Author.AvatarUrl,
                    // It isn't sent on chat messages
                    LastActivity = null
                },
                Message = chatMessage.Message,
                MessageUuid = chatMessage.MessageUuid,
                MessageRaw = chatMessage.MessageRaw,
                RoomId = chatMessage.RoomId,
                MessageRawHtmlDecoded = WebUtility.HtmlDecode(chatMessage.MessageRaw),
                MessageDate = DateTimeOffset.FromUnixTimeSeconds(chatMessage.MessageDate)
            };
            
            if (chatMessage.MessageEditDate == 0)
            {
                model.MessageEditDate = null;
            }
            else
            {
                model.MessageEditDate = DateTimeOffset.FromUnixTimeSeconds(chatMessage.MessageEditDate);
            }

            messages.Add(model);
        }
        _logger.Debug($"Received {messages.Count} chat messages");
        if (messages.Count == 1)
        {
            _logger.Debug($"{JsonSerializer.Serialize(messages[0])}");
        }

        try
        {
            OnMessages?.Invoke(this, messages, data);
        }
        catch (Exception e)
        {
            _logger.Error("Our handler for chat messages threw an exception");
            _logger.Error(e);
        }
    }

    private void WsChatUsersJoined(ResponseMessage message)
    {
        var data = JsonSerializer.Deserialize<UsersJsonModel>(message.Text!);
        var users = new List<UserModel>();
        foreach (var user in data!.Users.Keys)
        {
            users.Add(new UserModel
            {
                Id = int.Parse(user),
                Username = data.Users[user].Username,
                AvatarUrl = data.Users[user].AvatarUrl,
                LastActivity = DateTimeOffset.FromUnixTimeSeconds(data.Users[user].LastActivity)
            });
        }
        var usersJoined= data.Users.Select(user => int.Parse(user.Key)).ToList();
        _logger.Debug($"Following users have joined: {string.Join(',', usersJoined)}");
        OnUsersJoined?.Invoke(this, users, data);
    }

    private void WsChatUsersParted(ResponseMessage message)
    {
        // {"user":{"1337":false}}
        var data = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, bool>>>(message.Text!);
        var usersParted = data!["user"].Select(user => int.Parse(user.Key)).ToList();
        _logger.Debug($"Following users have parted: {string.Join(',', usersParted)}");
        OnUsersParted?.Invoke(this, usersParted);
    }

    private void WsPermissions(ResponseMessage message)
    {
        var data = JsonSerializer.Deserialize<JsonElement>(message.Text!);
        var permissions = data.GetProperty("permissions").Deserialize<PermissionsJsonModel>();
        try
        {
            OnPermissions?.Invoke(this, permissions!);
        }
        catch (Exception e)
        {
            _logger.Error("Caught error when invoking OnPermissions");
            _logger.Error(e);
        }
    }

    private void WsSystemMessage(ResponseMessage message)
    {
        var msg = JsonSerializer.Deserialize<JsonElement>(message.Text!).GetProperty("system").GetString();
        try
        {
            OnSystemMessage?.Invoke(this, msg!);
        }
        catch (Exception e)
        {
            _logger.Error("Caught error when invoking OnSystemMessage");
            _logger.Error(e);
        }
    }

    private void WsWhisper(ResponseMessage message)
    {
        var data = JsonSerializer.Deserialize<JsonElement>(message.Text!).GetProperty("whisper")
            .Deserialize<WhisperJsonModel>();
        var model = new WhisperModel
        {
            Author = new UserModel
            {
                Id = data!.Author.Id,
                Username = data.Author.Username,
                AvatarUrl = data.Author.AvatarUrl
            },
            Recipient = new UserModel
            {
                Id = data.Recipient.Id,
                Username = data.Recipient.Username,
                AvatarUrl = data.Recipient.AvatarUrl
            },
            Message = data.Message,
            MessageRaw = data.MessageRaw,
            MessageRawHtmlDecoded = WebUtility.HtmlDecode(data.MessageRaw),
            MessageDate = DateTimeOffset.FromUnixTimeSeconds(data.MessageDate)
        };
        try
        {
            OnWhisper?.Invoke(this, model);
        }
        catch (Exception e)
        {
            _logger.Error("WS handler for whisper threw an exception when invoking OnWhisper");
            _logger.Error(e);
        }
    }

    private void WsMotd(ResponseMessage message)
    {
        var motdElement = JsonSerializer.Deserialize<JsonElement>(message.Text!).GetProperty("motd");
            //.Deserialize<MessagesJsonModel.MessageModel>();        

        if (motdElement.ValueKind == JsonValueKind.Null) return;
        var msg = motdElement.Deserialize<MessagesJsonModel.MessageModel>();

        var model = new MessageModel
        {
            Author = new UserModel
            {
                Id = msg!.Author.Id,
                Username = msg.Author.Username,
                AvatarUrl = msg.Author.AvatarUrl,
                // It isn't sent on chat messages
                LastActivity = null
            },
            Message = msg.Message,
            MessageUuid = msg.MessageUuid,
            MessageRaw = msg.MessageRaw,
            RoomId = msg.RoomId,
            MessageRawHtmlDecoded = WebUtility.HtmlDecode(msg.MessageRaw),
            MessageDate = DateTimeOffset.FromUnixTimeSeconds(msg.MessageDate)
        };
            
        if (msg.MessageEditDate == 0)
        {
            model.MessageEditDate = null;
        }
        else
        {
            model.MessageEditDate = DateTimeOffset.FromUnixTimeSeconds(msg.MessageEditDate);
        }
        
        try
        {
            OnMotd?.Invoke(this, model);
        }
        catch (Exception e)
        {
            _logger.Error("The handler for MOTD messages threw an exception");
            _logger.Error(e);
        }
    }
}

public class WebSocketNotInitializedException : Exception;
