using System.Web;
using Discord;
using Discord.WebSocket;
using Lightquark.Types;
using Lightquark.Types.EventBus;
using Lightquark.Types.EventBus.Events;
using Lightquark.Types.EventBus.Messages;
using MongoDB.Bson;
using MongoDB.Driver;
using Newtonsoft.Json.Linq;
using Quarkcord.Objects;

namespace Quarkcord;

public class QuarkcordPlugin : IPlugin
{
    private static Task Log(LogMessage msg)
    {
        Console.WriteLine($"[Quarkcord] {msg.ToString()}");
        return Task.CompletedTask;
    }

    private string? _token;
    private string? _botUserId;
    private Lightquark.Types.Mongo.IUser? _user;
    private DiscordSocketClient? _client;
    private readonly ManualResetEvent _eventBusGetEvent = new(false);
    private IEventBus _eventBus = null!;
    private NetworkInformation? _networkInformation;
    private MongoClient _mongoClient = null!;
    private IMongoDatabase _database = null!;
    private IMongoCollection<MessagePair> MessagePairs => _database.GetCollection<MessagePair>("messages");
    private IMongoCollection<ChannelPair> ChannelPairs => _database.GetCollection<ChannelPair>("channels");
    private List<ChannelPair> _bridgeChannels = null!;

    public void Initialize(IEventBus eventBus)
    {
        Task.Run(async () =>
        {
            eventBus.Subscribe<BusReadyEvent>(_ => _eventBusGetEvent.Set());
            _eventBusGetEvent.WaitOne();
            _eventBus = eventBus;
            var filePath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "lightquark",
                "quarkcord");
            string mongoConnectionString;
            string mongoDb;
            if (File.Exists(filePath))
            {
                var text = (await File.ReadAllTextAsync(filePath)).Trim().Split(";");
                _token = text[0];
                _botUserId = text[1];
                mongoConnectionString = text[2];
                mongoDb = text[3];
            }
            else
            {
                await File.WriteAllTextAsync(filePath,
                    "INSERT_DISCORD_TOKEN_HERE;INSERT_LQ_BOT_USER_ID_HERE;INSERT_MONGO_CONNECTION_STRING_HERE;INSERT_MONGO_DB_HERE");
                _token = "INSERT_DISCORD_TOKEN_HERE";
                _botUserId = "INSERT_LQ_BOT_USER_ID_HERE";
                mongoConnectionString = "INSERT_MONGO_CONNECTION_STRING_HERE";
                mongoDb = "INSERT_MONGO_DB_HERE";
            }

            if (_token == "INSERT_DISCORD_TOKEN_HERE") throw new Exception($"Please add discord token to {filePath}");
            if (_botUserId == "INSERT_LQ_BOT_USER_ID_HERE")
                throw new Exception($"Please add bot user id to {filePath}");
            if (mongoConnectionString == "INSERT_MONGO_CONNECTION_STRING_HERE")
                throw new Exception($"Please add mongo connection string to {filePath}");
            if (mongoDb == "INSERT_MONGO_DB_HERE") throw new Exception($"Please add mongo db to {filePath}");

            _mongoClient = new MongoClient(mongoConnectionString);
            _database = _mongoClient.GetDatabase(mongoDb);

            var channelPairCursor = await ChannelPairs.FindAsync(new BsonDocument());
            var channelPairs = await channelPairCursor.ToListAsync();
            _bridgeChannels = channelPairs;
            if (_bridgeChannels.Count == 0)
            {
                await ChannelPairs.InsertOneAsync(new ChannelPair
                {
                    DiscordId = 0,
                    Id = ObjectId.GenerateNewId(),
                    LqId = ObjectId.Empty
                });
                await MessagePairs.InsertOneAsync(new MessagePair
                {
                    DiscordId = 0,
                    Id = ObjectId.GenerateNewId(),
                    LqId = ObjectId.Empty
                });
            }

            eventBus.Publish(new GetUserMessage
            {
                UserId = new ObjectId(_botUserId),
                Callback = user =>
                {
                    _user = user;
                    _eventBusGetEvent.Set();
                }
            });
            _eventBusGetEvent.WaitOne();
            _eventBusGetEvent.Reset();
            eventBus.Publish(new GetNetworkMessage
            {
                Callback = network =>
                {
                    _networkInformation = network;
                    _eventBusGetEvent.Set();
                }
            });
            _eventBusGetEvent.WaitOne();
            _client = new DiscordSocketClient(new DiscordSocketConfig
            {
                GatewayIntents = (GatewayIntents)130813
            });
            _client.Log += Log;
            await Log(new LogMessage(LogSeverity.Info, "Quarkcord", "Logging in"));
            await _client.LoginAsync(TokenType.Bot, _token);
            _client.Ready += () =>
            {
                Log(new LogMessage(LogSeverity.Info, "Quarkcord", "Connected!"));
                return Task.CompletedTask;
            };

            _client.MessageReceived += DiscordMessageReceived;
            _client.MessageUpdated += DiscordMessageUpdated;
            _client.MessageDeleted += DiscordMessageDeleted;
            _client.ReactionAdded += DiscordReactionAdded;

            eventBus.Subscribe<MessageCreateEvent>(LqMessageReceived);
            eventBus.Subscribe<MessageDeleteEvent>(LqMessageDeleted);

            await _client.StartAsync();
        });
    }

    private async void LqMessageDeleted(MessageDeleteEvent deleteEvent)
    {
        try
        {
            if (_bridgeChannels.All(bc => bc.LqId != deleteEvent.Message.ChannelId)) return;
            var bridgeChannel = _bridgeChannels.Find(bc => bc.LqId == deleteEvent.Message.ChannelId);
            if (_client!.GetChannel(bridgeChannel!.DiscordId) is not ITextChannel discordChannel) return;
            var messagePairCursor = await MessagePairs.FindAsync(mp => mp.LqId == deleteEvent.Message.Id);
            var messagePair = await messagePairCursor.FirstOrDefaultAsync();
            if (messagePair == null) return;
            await discordChannel.DeleteMessageAsync(messagePair.DiscordId);
            await MessagePairs.DeleteOneAsync(mp => mp.Id == messagePair.Id);
        }
        catch (Exception _)
        {
            // Plugin error ignored
        }
    }

    private async void LqMessageReceived(MessageCreateEvent createEvent)
    {
        try
        {
            if (createEvent.Message.Author!.Id == _user!.Id) return;
            if (_bridgeChannels.All(bc => bc.LqId != createEvent.Message.ChannelId)) return;
            var bridgeChannel = _bridgeChannels.Find(bc => bc.LqId == createEvent.Message.ChannelId);
            if (_client!.GetChannel(bridgeChannel!.DiscordId) is not ITextChannel discordChannel) return;
            var webhooks = await discordChannel.GetWebhooksAsync();
            var webhook = webhooks.FirstOrDefault(w => w.Name == $"Quarkcord {_networkInformation?.Name}")
                          ?? await discordChannel.CreateWebhookAsync($"Quarkcord {_networkInformation?.Name}");
            var webhookClient = new Discord.Webhook.DiscordWebhookClient(webhook.Id, webhook.Token);
            var username =
                $"{createEvent.Message.Author.Username} via {createEvent.Message.UserAgent} ({_networkInformation!.Name})";
            if (username.Length > 80)
            {
                username = $"{createEvent.Message.Author.Username} ({_networkInformation!.Name})";
            }

            if (username.Length > 80)
            {
                var toRemove = $" ({_networkInformation!.Name})".Length;
                username = $"{createEvent.Message.Author.Username[..(80 - toRemove)]} ({_networkInformation!.Name})";
            }

            var message = await webhookClient.SendMessageAsync(
                createEvent.Message.Content?.Length <= 2000
                    ? createEvent.Message.Content
                    : "A message was sent but it is too long. Please view it on Lightquark",
                false,
                createEvent.Message.Attachments.Select(a => a.MimeType.StartsWith("image")
                    ? new EmbedBuilder().WithImageUrl(a.Url.ToString()).Build()
                    : new EmbedBuilder().WithTitle($"{a.Filename} ({HumanReadable.BytesToString(a.Size)})")
                        .WithUrl(a.Url.ToString()).Build()),
                username,
                createEvent.Message.Author.AvatarUriGetter.ToString(),
                null,
                AllowedMentions.None);

            var messagePair = new MessagePair
            {
                Id = ObjectId.GenerateNewId(),
                LqId = createEvent.Message.Id,
                DiscordId = message
            };
            await MessagePairs.InsertOneAsync(messagePair);
        }
        catch (Exception _)
        {
            // Ignore plugin error
        }
    }

    private async Task DiscordMessageUpdated(Cacheable<IMessage, ulong> oldMessageParam, SocketMessage messageParam,
        ISocketMessageChannel channelParam)
    {
        try
        {
            if (messageParam is not SocketUserMessage message) return;
            if (message.Author.Id == _client!.CurrentUser.Id) return;
            if (_user == null)
            {
                await Log(new LogMessage(LogSeverity.Warning, "Quarkcord", "Message received but bot user is null"));
                return;
            }

            if (_bridgeChannels.All(bc => bc.DiscordId != message.Channel.Id)) return;
            var bridgeChannel = _bridgeChannels.Find(bc => bc.DiscordId == message.Channel.Id);
            var messagePairCursor = await MessagePairs.FindAsync(mp => mp.DiscordId == message.Id);
            var messagePair = await messagePairCursor.FirstOrDefaultAsync();
            if (messagePair == null)
            {
                // Somehow we didn't have this one, lets bridge it now!
                await SendOrUpdateLqMessage(message, bridgeChannel!);
            }
            else
            {
                await SendOrUpdateLqMessage(message, bridgeChannel!, true, messagePair);
            }
        }
        catch (Exception _)
        {
            //
        }
    }


    private async Task DiscordReactionAdded(Cacheable<IUserMessage, ulong> messageParam,
        Cacheable<IMessageChannel, ulong> channelParam, SocketReaction reactionParam)
    {
        try
        {
            if (_bridgeChannels.All(bc => bc.DiscordId != channelParam.Id)) return;
            var bridgeChannel = _bridgeChannels.Find(bc => bc.DiscordId == channelParam.Id);
            var discordMessageId = messageParam.Id;
            var messagePairCursor = await MessagePairs.FindAsync(m => m.DiscordId == discordMessageId);
            var messagePair = await messagePairCursor.FirstOrDefaultAsync();
            if (messagePair == null) return;
            var message = await messageParam.DownloadAsync();

            var specialAttributes = new JArray
            {
                new JObject
                {
                    ["type"] = "reply",
                    ["replyTo"] = messagePair.LqId.ToString()
                }
            };

            _eventBus.Publish(new CreateMessageMessage
            {
                Message = new LqMessage
                {
                    VirtualAuthors = [_user!],
                    ChannelId = bridgeChannel!.LqId,
                    Id = ObjectId.GenerateNewId(),
                    AuthorId = _user!.Id,
                    Content = $"{(reactionParam.User.GetValueOrDefault() as SocketGuildUser)?.Nickname
                                 ?? reactionParam.User.GetValueOrDefault().GlobalName
                                 ?? reactionParam.User.GetValueOrDefault().Username
                                 ?? "Someone"} reacted with {reactionParam.Emote}",
                    UserAgent = "Quarkcord",
                    Timestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(),
                    Edited = false,
                    Attachments = [],
                    SpecialAttributes = specialAttributes
                }
            });
        }
        catch (Exception _)
        {
            // shut up
        }
    }

    private async Task DiscordMessageDeleted(Cacheable<IMessage, ulong> messageParam,
        Cacheable<IMessageChannel, ulong> channelParam)
    {
        try
        {
            if (_bridgeChannels.All(bc => bc.DiscordId != channelParam.Id)) return;
            var discordMessageId = messageParam.Id;
            var messagePairCursor = await MessagePairs.FindAsync(m => m.DiscordId == discordMessageId);
            var messagePair = await messagePairCursor.FirstOrDefaultAsync();
            if (messagePair == null) return;

            _eventBus.Publish(new DeleteMessageMessage
            {
                MessageId = messagePair.LqId
            });

            await MessagePairs.DeleteOneAsync(mp => mp.Id == messagePair.Id);
        }
        catch (Exception _)
        {
            // Ignore
        }
    }

    private async Task SendOrUpdateLqMessage(SocketUserMessage message, ChannelPair bridgeChannel, bool update = false,
        MessagePair? existingMessagePair = null)
    {
        if (message.Author.Username.EndsWith($"({_networkInformation!.Name})")) return;
        var specialAttributes = new JArray
        {
            new JObject
            {
                ["type"] = "botMessage",
                ["username"] = (message.Author as SocketGuildUser)?.Nickname ??
                               message.Author.GlobalName ?? message.Author.Username,
                ["avatarUri"] =
                    $"{_networkInformation!.CdnBaseUrl}/external/{HttpUtility.UrlEncode(message.Author.GetDisplayAvatarUrl())}"
            }
        };
        if (message.ReferencedMessage != null)
        {
            var replyCursor = await MessagePairs.FindAsync(mp => mp.DiscordId == message.ReferencedMessage.Id);
            var reply = await replyCursor.FirstOrDefaultAsync();
            if (reply != null)
            {
                specialAttributes.Add(new JObject
                {
                    ["type"] = "reply",
                    ["replyTo"] = reply.LqId.ToString()
                });
            }
        }

        var lqMessageId = existingMessagePair?.LqId ?? ObjectId.GenerateNewId();
        var messagePair = existingMessagePair ?? new MessagePair
        {
            DiscordId = message.Id,
            Id = ObjectId.GenerateNewId(),
            LqId = lqMessageId
        };
        if (!update)
        {
            await MessagePairs.InsertOneAsync(messagePair);
        }

        var lqAttachments = message.Attachments.Select(a => new LqAttachment
        {
            FileId = ObjectId.Empty,
            Filename = a.Filename,
            MimeType = a.ContentType,
            Size = a.Size,
            Width = a.Width,
            Height = a.Height,
            Url = new Uri($"{_networkInformation!.CdnBaseUrl}/external/{HttpUtility.UrlEncode(a.Url)}")
        }).ToArray();
        LqMessage lqMessage;
        if (message.Attachments.Count == 0 && message.Content.Length == 0)
        {
            specialAttributes.Add(new JObject
            {
                ["type"] = "clientAttributes",
                ["discordMessageId"] = message.Id,
                ["quarkcord"] = true,
                ["quarkcordUnsupported"] = true
            });
            lqMessage = new LqMessage
            {
                Id = lqMessageId,
                AuthorId = _user!.Id,
                Content = "Discord message with unsupported content",
                ChannelId = bridgeChannel!.LqId,
                UserAgent = "Quarkcord",
                Timestamp = message.Timestamp.ToUnixTimeMilliseconds(),
                VirtualAuthors = [_user],
                Edited = update,
                Attachments = [],
                SpecialAttributes = specialAttributes
            };
        }
        else
        {
            specialAttributes.Add(new JObject
            {
                ["type"] = "clientAttributes",
                ["discordMessageId"] = message.Id,
                ["quarkcord"] = true
            });
            lqMessage = new LqMessage
            {
                Id = lqMessageId,
                AuthorId = _user!.Id,
                Content = message.Content,
                ChannelId = bridgeChannel!.LqId,
                UserAgent = "Quarkcord",
                Timestamp = message.Timestamp.ToUnixTimeMilliseconds(),
                VirtualAuthors = [_user],
                Edited = update,
                Attachments = lqAttachments,
                SpecialAttributes = specialAttributes
            };
        }

        if (update)
        {
            _eventBus.Publish(new EditMessageMessage
            {
                Message = lqMessage
            });
        }
        else
        {
            _eventBus.Publish(new CreateMessageMessage
            {
                Message = lqMessage
            });
        }
    }

    private async Task DiscordMessageReceived(SocketMessage messageParam)
    {
        try
        {
            if (messageParam is not SocketUserMessage message) return;
            if (message.Author.Id == _client!.CurrentUser.Id) return;
            if (_user == null)
            {
                await Log(new LogMessage(LogSeverity.Warning, "Quarkcord", "Message received but bot user is null"));
                return;
            }

            if (_bridgeChannels.All(bc => bc.DiscordId != message.Channel.Id)) return;
            var bridgeChannel = _bridgeChannels.Find(bc => bc.DiscordId == message.Channel.Id);

            await SendOrUpdateLqMessage(message, bridgeChannel!);
        }
        catch (Exception _)
        {
            // Plugin error ignor
        }
    }
}

public class LqMessage : Lightquark.Types.Mongo.IMessage
{
    public ObjectId Id { get; set; }
    public ObjectId AuthorId { get; set; }
    public string? Content { get; set; }
    public ObjectId ChannelId { get; set; }
    public string UserAgent { get; set; }
    public long Timestamp { get; set; }
    public bool Edited { get; set; }
    public Lightquark.Types.Mongo.IAttachment[] Attachments { get; set; }
    public JArray SpecialAttributes { get; set; }
    public Lightquark.Types.Mongo.IUser? Author => VirtualAuthors?.FirstOrDefault();
    public Lightquark.Types.Mongo.IUser[]? VirtualAuthors { get; set; }
}

public class LqAttachment : Lightquark.Types.Mongo.IAttachment
{
    public Uri Url { get; set; }
    public long Size { get; set; }
    public string MimeType { get; set; }
    public string Filename { get; set; }
    public ObjectId FileId { get; set; }
    public int? Height { get; set; }
    public int? Width { get; set; }
}