using SharpChat.Events;
using SharpChat.EventStorage;
using SharpChat.S2CPackets;
using SharpChat.Snowflake;
using System.Net;

namespace SharpChat {
    public class ChatContext {
        public record ChannelUserAssoc(long UserId, string ChannelName);

        public readonly SemaphoreSlim ContextAccess = new(1, 1);

        public SnowflakeGenerator SnowflakeGenerator { get; } = new();
        public RandomSnowflake RandomSnowflake { get; }

        public HashSet<ChatChannel> Channels { get; } = [];
        public HashSet<ChatConnection> Connections { get; } = [];
        public HashSet<ChatUser> Users { get; } = [];
        public IEventStorage Events { get; }
        public HashSet<ChannelUserAssoc> ChannelUsers { get; } = [];
        public Dictionary<long, RateLimiter> UserRateLimiters { get; } = [];
        public Dictionary<long, ChatChannel> UserLastChannel { get; } = [];

        public ChatContext(IEventStorage evtStore) {
            Events = evtStore ?? throw new ArgumentNullException(nameof(evtStore));
            RandomSnowflake = new(SnowflakeGenerator);
        }

        public void DispatchEvent(IChatEvent eventInfo) {
            if(eventInfo is MessageCreateEvent mce) {
                if(mce.IsBroadcast) {
                    Send(new CommandResponseS2CPacket(RandomSnowflake.Next(), LCR.BROADCAST, false, mce.MessageText));
                } else if(mce.IsPrivate) {
                    // The channel name returned by GetDMChannelName should not be exposed to the user, instead @<Target User> should be displayed
                    // e.g. nook sees @Arysil and Arysil sees @nook

                    // this entire routine is garbage, channels should probably in the db
                    if(!mce.ChannelName.StartsWith('@'))
                        return;

                    IEnumerable<long> uids = mce.ChannelName[1..].Split('-', 3).Select(u => long.TryParse(u, out long up) ? up : -1);
                    if(uids.Count() != 2)
                        return;

                    IEnumerable<ChatUser> users = Users.Where(u => uids.Any(uid => uid == u.UserId));
                    ChatUser? target = users.FirstOrDefault(u => u.UserId != mce.SenderId);
                    if(target == null)
                        return;

                    foreach(ChatUser user in users)
                        SendTo(user, new ChatMessageAddS2CPacket(
                            mce.MessageId,
                            DateTimeOffset.Now,
                            mce.SenderId,
                            mce.SenderId == user.UserId ? $"{target.LegacyName} {mce.MessageText}" : mce.MessageText,
                            mce.IsAction,
                            true
                        ));
                } else {
                    ChatChannel? channel = Channels.FirstOrDefault(c => c.NameEquals(mce.ChannelName));
                    if(channel is not null)
                        SendTo(channel, new ChatMessageAddS2CPacket(
                            mce.MessageId,
                            DateTimeOffset.Now,
                            mce.SenderId,
                            mce.MessageText,
                            mce.IsAction,
                            false
                        ));
                }

                Events.AddEvent(
                    mce.MessageId, "msg:add",
                    mce.ChannelName,
                    mce.SenderId, mce.SenderName, mce.SenderColour, mce.SenderRank, mce.SenderNickName, mce.SenderPerms,
                    new { text = mce.MessageText },
                    (mce.IsBroadcast ? StoredEventFlags.Broadcast : 0)
                    | (mce.IsAction ? StoredEventFlags.Action : 0)
                    | (mce.IsPrivate ? StoredEventFlags.Private : 0)
                );
                return;
            }
        }

        public void Update() {
            foreach(ChatConnection conn in Connections)
                if(!conn.IsDisposed && conn.HasTimedOut) {
                    conn.Dispose();
                    Logger.Write($"Nuked connection {conn.Id} associated with {conn.User}.");
                }

            Connections.RemoveWhere(conn => conn.IsDisposed);

            foreach(ChatUser user in Users)
                if(!Connections.Any(conn => conn.User == user)) {
                    HandleDisconnect(user, UserDisconnectReason.TimeOut);
                    Logger.Write($"Timed out {user} (no more connections).");
                }
        }

        public void SafeUpdate() {
            ContextAccess.Wait();
            try {
                Update();
            } finally {
                ContextAccess.Release();
            }
        }

        public bool IsInChannel(ChatUser user, ChatChannel channel) {
            return ChannelUsers.Contains(new ChannelUserAssoc(user.UserId, channel.Name));
        }

        public string[] GetUserChannelNames(ChatUser user) {
            return [.. ChannelUsers.Where(cu => cu.UserId == user.UserId).Select(cu => cu.ChannelName)];
        }

        public ChatChannel[] GetUserChannels(ChatUser user) {
            string[] names = GetUserChannelNames(user);
            return [.. Channels.Where(c => names.Any(n => c.NameEquals(n)))];
        }

        public long[] GetChannelUserIds(ChatChannel channel) {
            return [.. ChannelUsers.Where(cu => channel.NameEquals(cu.ChannelName)).Select(cu => cu.UserId)];
        }

        public ChatUser[] GetChannelUsers(ChatChannel channel) {
            long[] ids = GetChannelUserIds(channel);
            return [.. Users.Where(u => ids.Contains(u.UserId))];
        }

        public void UpdateUser(
            ChatUser user,
            string? userName = null,
            string? nickName = null,
            ChatColour? colour = null,
            ChatUserStatus? status = null,
            string? statusText = null,
            int? rank = null,
            ChatUserPermissions? perms = null,
            bool? isSuper = null,
            bool silent = false
        ) {
            ArgumentNullException.ThrowIfNull(user);

            bool hasChanged = false;
            string previousName = string.Empty;

            if(userName != null && !user.UserName.Equals(userName)) {
                user.UserName = userName;
                hasChanged = true;
            }

            if(nickName != null && !user.NickName.Equals(nickName)) {
                if(!silent)
                    previousName = string.IsNullOrWhiteSpace(user.NickName) ? user.UserName : user.NickName;

                user.NickName = nickName;
                hasChanged = true;
            }

            if(colour.HasValue && user.Colour != colour.Value) {
                user.Colour = colour.Value;
                hasChanged = true;
            }

            if(status.HasValue && user.Status != status.Value) {
                user.Status = status.Value;
                hasChanged = true;
            }

            if(statusText != null && !user.StatusText.Equals(statusText)) {
                user.StatusText = statusText;
                hasChanged = true;
            }

            if(rank != null && user.Rank != rank) {
                user.Rank = (int)rank;
                hasChanged = true;
            }

            if(perms.HasValue && user.Permissions != perms) {
                user.Permissions = perms.Value;
                hasChanged = true;
            }

            if(isSuper.HasValue) {
                user.IsSuper = isSuper.Value;
                hasChanged = true;
            }

            if(hasChanged)
                SendToUserChannels(user, new UserUpdateS2CPacket(RandomSnowflake.Next(), user, previousName));
        }

        public void BanUser(ChatUser user, TimeSpan duration, UserDisconnectReason reason = UserDisconnectReason.Kicked) {
            if(duration > TimeSpan.Zero) {
                DateTimeOffset expires = duration >= TimeSpan.MaxValue ? DateTimeOffset.MaxValue : DateTimeOffset.Now + duration;
                SendTo(user, new ForceDisconnectS2CPacket(ForceDisconnectReason.Banned, expires));
            } else
                SendTo(user, new ForceDisconnectS2CPacket(ForceDisconnectReason.Kicked));

            foreach(ChatConnection conn in Connections)
                if(conn.User == user)
                    conn.Dispose();
            Connections.RemoveWhere(conn => conn.IsDisposed);

            HandleDisconnect(user, reason);
        }

        public void HandleJoin(ChatUser user, ChatChannel chan, ChatConnection conn, int maxMsgLength) {
            if(!IsInChannel(user, chan)) {
                long msgId = RandomSnowflake.Next();
                SendTo(chan, new UserConnectS2CPacket(msgId, DateTimeOffset.Now, user));
                Events.AddEvent(msgId, "user:connect", chan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, null, StoredEventFlags.Log);
            }

            conn.Send(new AuthSuccessS2CPacket(user, chan, maxMsgLength));
            conn.Send(new ContextUsersS2CPacket(GetChannelUsers(chan).Except([user]).OrderByDescending(u => u.Rank)));

            foreach(StoredEventInfo msg in Events.GetChannelEventLog(chan.Name))
                conn.Send(new ContextMessageS2CPacket(msg));

            conn.Send(new ContextChannelsS2CPacket(Channels.Where(c => c.Rank <= user.Rank)));

            Users.Add(user);

            ChannelUsers.Add(new ChannelUserAssoc(user.UserId, chan.Name));
            UserLastChannel[user.UserId] = chan;
        }

        public void HandleDisconnect(ChatUser user, UserDisconnectReason reason = UserDisconnectReason.Leave) {
            UpdateUser(user, status: ChatUserStatus.Offline);
            Users.Remove(user);
            UserLastChannel.Remove(user.UserId);

            ChatChannel[] channels = GetUserChannels(user);

            foreach(ChatChannel chan in channels) {
                ChannelUsers.Remove(new ChannelUserAssoc(user.UserId, chan.Name));

                long msgId = RandomSnowflake.Next();
                SendTo(chan, new UserDisconnectS2CPacket(msgId, DateTimeOffset.Now, user, reason));
                Events.AddEvent(msgId, "user:disconnect", chan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, new { reason = (int)reason }, StoredEventFlags.Log);

                if(chan.IsTemporary && chan.IsOwner(user))
                    RemoveChannel(chan);
            }
        }

        public void SwitchChannel(ChatUser user, ChatChannel chan, string password) {
            if(UserLastChannel.TryGetValue(user.UserId, out ChatChannel? ulc) && chan == ulc) {
                ForceChannel(user);
                return;
            }

            if(!user.Can(ChatUserPermissions.JoinAnyChannel) && chan.IsOwner(user)) {
                if(chan.Rank > user.Rank) {
                    SendTo(user, new CommandResponseS2CPacket(RandomSnowflake.Next(), LCR.CHANNEL_INSUFFICIENT_HIERARCHY, true, chan.Name));
                    ForceChannel(user);
                    return;
                }

                if(!string.IsNullOrEmpty(chan.Password) && chan.Password != password) {
                    SendTo(user, new CommandResponseS2CPacket(RandomSnowflake.Next(), LCR.CHANNEL_INVALID_PASSWORD, true, chan.Name));
                    ForceChannel(user);
                    return;
                }
            }

            ForceChannelSwitch(user, chan);
        }

        public void ForceChannelSwitch(ChatUser user, ChatChannel chan) {
            if(!Channels.Contains(chan))
                return;

            ChatChannel oldChan = UserLastChannel[user.UserId];

            long leaveId = RandomSnowflake.Next();
            SendTo(oldChan, new UserChannelLeaveS2CPacket(leaveId, user));
            Events.AddEvent(leaveId, "chan:leave", oldChan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, null, StoredEventFlags.Log);

            long joinId = RandomSnowflake.Next();
            SendTo(chan, new UserChannelJoinS2CPacket(joinId, user));
            Events.AddEvent(joinId, "chan:join", chan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, null, StoredEventFlags.Log);

            SendTo(user, new ContextClearS2CPacket(ContextClearMode.MessagesUsers));
            SendTo(user, new ContextUsersS2CPacket(GetChannelUsers(chan).Except([user]).OrderByDescending(u => u.Rank)));

            foreach(StoredEventInfo msg in Events.GetChannelEventLog(chan.Name))
                SendTo(user, new ContextMessageS2CPacket(msg));

            ForceChannel(user, chan);

            ChannelUsers.Remove(new ChannelUserAssoc(user.UserId, oldChan.Name));
            ChannelUsers.Add(new ChannelUserAssoc(user.UserId, chan.Name));
            UserLastChannel[user.UserId] = chan;

            if(oldChan.IsTemporary && oldChan.IsOwner(user))
                RemoveChannel(oldChan);
        }

        public void Send(S2CPacket packet) {
            ArgumentNullException.ThrowIfNull(packet);

            foreach(ChatConnection conn in Connections)
                if(conn.IsAlive && conn.User is not null)
                    conn.Send(packet);
        }

        public void SendTo(ChatUser user, S2CPacket packet) {
            ArgumentNullException.ThrowIfNull(user);
            ArgumentNullException.ThrowIfNull(packet);

            foreach(ChatConnection conn in Connections)
                if(conn.IsAlive && conn.User == user)
                    conn.Send(packet);
        }

        public void SendTo(ChatChannel channel, S2CPacket packet) {
            ArgumentNullException.ThrowIfNull(channel);
            ArgumentNullException.ThrowIfNull(packet);

            // might be faster to grab the users first and then cascade into that SendTo
            IEnumerable<ChatConnection> conns = Connections.Where(c => c.IsAlive && c.User is not null && IsInChannel(c.User, channel));
            foreach(ChatConnection conn in conns)
                conn.Send(packet);
        }

        public void SendToUserChannels(ChatUser user, S2CPacket packet) {
            ArgumentNullException.ThrowIfNull(user);
            ArgumentNullException.ThrowIfNull(packet);

            IEnumerable<ChatChannel> chans = Channels.Where(c => IsInChannel(user, c));
            IEnumerable<ChatConnection> conns = Connections.Where(conn => conn.IsAlive && conn.User is not null && ChannelUsers.Any(cu => cu.UserId == conn.User.UserId && chans.Any(chan => chan.NameEquals(cu.ChannelName))));
            foreach(ChatConnection conn in conns)
                conn.Send(packet);
        }

        public IPAddress[] GetRemoteAddresses(ChatUser user) {
            return [.. Connections.Where(c => c.IsAlive && c.User == user).Select(c => c.RemoteAddress).Distinct()];
        }

        public void ForceChannel(ChatUser user, ChatChannel? chan = null) {
            ArgumentNullException.ThrowIfNull(user);

            if(chan == null && !UserLastChannel.TryGetValue(user.UserId, out chan))
                throw new ArgumentException("no channel???");

            SendTo(user, new UserChannelForceJoinS2CPacket(chan));
        }

        public void UpdateChannel(ChatChannel channel, bool? temporary = null, int? hierarchy = null, string? password = null) {
            ArgumentNullException.ThrowIfNull(channel);
            if(!Channels.Contains(channel))
                throw new ArgumentException("Provided channel is not registered with this manager.", nameof(channel));

            if(temporary.HasValue)
                channel.IsTemporary = temporary.Value;

            if(hierarchy.HasValue)
                channel.Rank = hierarchy.Value;

            if(password != null)
                channel.Password = password;

            // TODO: Users that no longer have access to the channel/gained access to the channel by the hierarchy change should receive delete and create packets respectively
            foreach(ChatUser user in Users.Where(u => u.Rank >= channel.Rank)) {
                SendTo(user, new ChannelUpdateS2CPacket(channel.Name, channel));
            }
        }

        public void RemoveChannel(ChatChannel channel) {
            if(channel == null || Channels.Count < 1)
                return;

            ChatChannel? defaultChannel = Channels.FirstOrDefault();
            if(defaultChannel is null)
                return;

            // Remove channel from the listing
            Channels.Remove(channel);

            // Move all users back to the main channel
            // TODO: Replace this with a kick. SCv2 supports being in 0 channels, SCv1 should force the user back to DefaultChannel.
            foreach(ChatUser user in GetChannelUsers(channel))
                SwitchChannel(user, defaultChannel, string.Empty);

            // Broadcast deletion of channel
            foreach(ChatUser user in Users.Where(u => u.Rank >= channel.Rank))
                SendTo(user, new ChannelDeleteS2CPacket(channel));
        }
    }
}