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

namespace SharpChat;

public class Context {
    public record ChannelUserAssoc(string UserId, string ChannelName);

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

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

    public HashSet<Channel> Channels { get; } = [];
    public HashSet<Connection> Connections { get; } = [];
    public HashSet<User> Users { get; } = [];
    public EventStorage.EventStorage Events { get; }
    public HashSet<ChannelUserAssoc> ChannelUsers { get; } = [];
    public Dictionary<string, RateLimiter> UserRateLimiters { get; } = [];
    public Dictionary<string, Channel> UserLastChannel { get; } = [];

    public Context(EventStorage.EventStorage evtStore) {
        Events = evtStore ?? throw new ArgumentNullException(nameof(evtStore));
        RandomSnowflake = new(SnowflakeGenerator);
    }

    public async Task DispatchEvent(ChatEvent eventInfo) {
        if(eventInfo is MessageCreateEvent mce) {
            if(mce.IsBroadcast) {
                await 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<string> uids = mce.ChannelName[1..].Split('-', 3).Select(u => (long.TryParse(u, out long up) ? up : -1).ToString());
                if(uids.Count() != 2)
                    return;

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

                foreach(User user in users)
                    await SendTo(user, new ChatMessageAddS2CPacket(
                        mce.MessageId,
                        DateTimeOffset.Now,
                        mce.SenderId,
                        mce.SenderId == user.UserId ? $"{target.LegacyName} {mce.MessageText}" : mce.MessageText,
                        mce.IsAction,
                        true
                    ));
            } else {
                Channel? channel = Channels.FirstOrDefault(c => c.NameEquals(mce.ChannelName));
                if(channel is not null)
                    await 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 async Task Update() {
        foreach(Connection 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(User user in Users)
            if(!Connections.Any(conn => conn.User == user)) {
                Logger.Write($"Timing out {user} (no more connections).");
                await HandleDisconnect(user, UserDisconnectS2CPacket.Reason.TimeOut);
            }
    }

    public async Task SafeUpdate() {
        ContextAccess.Wait();
        try {
            await Update();
        } finally {
            ContextAccess.Release();
        }
    }

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

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

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

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

    public User[] GetChannelUsers(Channel channel) {
        string[] ids = GetChannelUserIds(channel);
        return [.. Users.Where(u => ids.Contains(u.UserId))];
    }

    public async Task UpdateUser(
        User user,
        string? userName = null,
        string? nickName = null,
        ColourInheritable? colour = null,
        UserStatus? status = null,
        string? statusText = null,
        int? rank = null,
        UserPermissions? perms = 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 = user.LegacyName;

            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(hasChanged) {
            if(!string.IsNullOrWhiteSpace(previousName))
                await SendToUserChannels(user, new CommandResponseS2CPacket(RandomSnowflake.Next(), LCR.NICKNAME_CHANGE, false, previousName, user.LegacyNameWithStatus));

            await SendToUserChannels(user, new UserUpdateS2CPacket(user.UserId, user.LegacyNameWithStatus, user.Colour, user.Rank, user.Permissions));
        }
    }

    public async Task BanUser(User user, TimeSpan duration, UserDisconnectS2CPacket.Reason reason = UserDisconnectS2CPacket.Reason.Kicked) {
        if(duration > TimeSpan.Zero) {
            DateTimeOffset expires = duration >= TimeSpan.MaxValue ? DateTimeOffset.MaxValue : DateTimeOffset.Now + duration;
            await SendTo(user, new ForceDisconnectS2CPacket(expires));
        } else
            await SendTo(user, new ForceDisconnectS2CPacket());

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

        await HandleDisconnect(user, reason);
    }

    public async Task HandleJoin(User user, Channel chan, Connection conn, int maxMsgLength) {
        if(!IsInChannel(user, chan)) {
            long msgId = RandomSnowflake.Next();
            await SendTo(chan, new UserConnectS2CPacket(msgId, DateTimeOffset.Now, user.UserId, user.LegacyNameWithStatus, user.Colour, user.Rank, user.Permissions));
            Events.AddEvent(msgId, "user:connect", chan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, null, StoredEventFlags.Log);
        }

        await conn.Send(new AuthSuccessS2CPacket(
            user.UserId,
            user.LegacyNameWithStatus,
            user.Colour,
            user.Rank,
            user.Permissions,
            chan.Name,
            maxMsgLength
        ));
        await conn.Send(new ContextUsersS2CPacket(
            GetChannelUsers(chan).Except([user]).OrderByDescending(u => u.Rank)
                .Select(u => new ContextUsersS2CPacket.Entry(
                    u.UserId,
                    u.LegacyNameWithStatus,
                    u.Colour,
                    u.Rank,
                    u.Permissions,
                    true
                ))
        ));

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

        await conn.Send(new ContextChannelsS2CPacket(
            Channels.Where(c => c.Rank <= user.Rank)
                .Select(c => new ContextChannelsS2CPacket.Entry(c.Name, c.HasPassword, c.IsTemporary))
        ));

        Users.Add(user);

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

    public async Task HandleDisconnect(User user, UserDisconnectS2CPacket.Reason reason = UserDisconnectS2CPacket.Reason.Leave) {
        await UpdateUser(user, status: UserStatus.Offline);
        Users.Remove(user);
        UserLastChannel.Remove(user.UserId);

        Channel[] channels = GetUserChannels(user);

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

            long msgId = RandomSnowflake.Next();
            await SendTo(chan, new UserDisconnectS2CPacket(msgId, DateTimeOffset.Now, user.UserId, user.LegacyNameWithStatus, 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))
                await RemoveChannel(chan);
        }
    }

    public async Task SwitchChannel(User user, Channel chan, string password) {
        if(UserLastChannel.TryGetValue(user.UserId, out Channel? ulc) && chan == ulc) {
            await ForceChannel(user);
            return;
        }

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

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

        await ForceChannelSwitch(user, chan);
    }

    public async Task ForceChannelSwitch(User user, Channel chan) {
        if(!Channels.Contains(chan))
            return;

        Channel oldChan = UserLastChannel[user.UserId];

        long leaveId = RandomSnowflake.Next();
        await SendTo(oldChan, new UserChannelLeaveS2CPacket(leaveId, user.UserId));
        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();
        await SendTo(chan, new UserChannelJoinS2CPacket(joinId, user.UserId, user.LegacyNameWithStatus, user.Colour, user.Rank, user.Permissions));
        Events.AddEvent(joinId, "chan:join", chan.Name, user.UserId, user.LegacyName, user.Colour, user.Rank, user.NickName, user.Permissions, null, StoredEventFlags.Log);

        await SendTo(user, new ContextClearS2CPacket(ContextClearS2CPacket.Mode.MessagesUsers));
        await SendTo(user, new ContextUsersS2CPacket(
            GetChannelUsers(chan).Except([user]).OrderByDescending(u => u.Rank)
                .Select(u => new ContextUsersS2CPacket.Entry(
                    u.UserId,
                    u.LegacyNameWithStatus,
                    u.Colour,
                    u.Rank,
                    u.Permissions,
                    true
                ))
        ));

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

        await 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))
            await RemoveChannel(oldChan);
    }

    public async Task Send(S2CPacket packet) {
        foreach(Connection conn in Connections)
            if(conn.IsAlive && conn.User is not null)
                await conn.Send(packet);
    }

    public async Task SendTo(User user, S2CPacket packet) {
        foreach(Connection conn in Connections)
            if(conn.IsAlive && conn.User == user)
                await conn.Send(packet);
    }

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

    public async Task SendToUserChannels(User user, S2CPacket packet) {
        IEnumerable<Channel> chans = Channels.Where(c => IsInChannel(user, c));
        IEnumerable<Connection> 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(Connection conn in conns)
            await conn.Send(packet);
    }

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

    public async Task ForceChannel(User user, Channel? chan = null) {
        ArgumentNullException.ThrowIfNull(user);

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

        await SendTo(user, new UserChannelForceJoinS2CPacket(chan.Name));
    }

    public async Task UpdateChannel(Channel 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(User user in Users.Where(u => u.Rank >= channel.Rank))
            await SendTo(user, new ChannelUpdateS2CPacket(channel.Name, channel.Name, channel.HasPassword, channel.IsTemporary));
    }

    public async Task RemoveChannel(Channel channel) {
        if(channel == null || Channels.Count < 1)
            return;

        Channel? 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(User user in GetChannelUsers(channel))
            await SwitchChannel(user, defaultChannel, string.Empty);

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