using SharpChat.Users;

namespace SharpChat.Channels;

public class ChannelsUsersContext(ChannelsContext channelsCtx, UsersContext usersCtx) {
    private readonly Dictionary<string, Dictionary<long, DateTimeOffset>> UserChannels = [];
    private readonly Dictionary<long, HashSet<string>> ChannelUsers = [];
    private readonly Dictionary<string, long> UserLastChannel = [];
    private readonly Lock @lock = new();

    private void UpdateUserLastChannel(string userId, long? channelId = null) {
        if(channelId.HasValue
            && UserLastChannel.TryGetValue(userId, out long lastChannelId)
            && channelId.Value != lastChannelId)
            return;

        if(UserChannels.TryGetValue(userId, out var userChannels) && userChannels.Count > 0)
            UserLastChannel[userId] = userChannels.OrderByDescending(kvp => kvp.Value).First().Key;
        else
            UserLastChannel.Remove(userId);
    }

    public void RemoveChannel(Channel channel)
        => RemoveChannel(channel.Id);

    public void RemoveChannel(long channelId) {
        lock(@lock) {
            if(!ChannelUsers.TryGetValue(channelId, out var channelUsers))
                return;
            ChannelUsers.Remove(channelId);

            foreach(string userId in channelUsers)
                if(UserChannels.TryGetValue(userId, out var userChannels)) {
                    userChannels.Remove(channelId);
                    UpdateUserLastChannel(userId, channelId);
                }
        }
    }

    public void RemoveUser(User user)
        => RemoveUser(user.UserId);

    public void RemoveUser(string userId) {
        lock(@lock) {
            if(!UserChannels.TryGetValue(userId, out var userChannels))
                return;
            UserChannels.Remove(userId);
            UserLastChannel.Remove(userId);

            foreach(long channelId in userChannels.Keys)
                if(ChannelUsers.TryGetValue(channelId, out var channelUsers))
                    channelUsers.Remove(userId);
        }
    }

    public void RecordChannelUserActivity(Channel channel, User user)
        => RecordChannelUserActivity(channel.Id, user.UserId);
    public void RecordChannelUserActivity(long channelId, User user)
        => RecordChannelUserActivity(channelId, user.UserId);
    public void RecordChannelUserActivity(Channel channel, string userId)
        => RecordChannelUserActivity(channel.Id, userId);

    public void RecordChannelUserActivity(long channelId, string userId) {
        lock(@lock) {
            if(!UserChannels.TryGetValue(userId, out var userChannels))
                throw new ArgumentException("Attempted to register activity for non-existent user.", nameof(userId));

            userChannels[channelId] = DateTimeOffset.UtcNow;
            UserLastChannel[userId] = channelId;
        }
    }

    public void AddChannelUser(Channel channel, User user)
        => AddChannelUser(channel.Id, user.UserId);

    public void AddChannelUser(long channelId, string userId) {
        lock(@lock) {
            if(!ChannelUsers.TryGetValue(channelId, out var channelUsers))
                ChannelUsers.Add(channelId, channelUsers = []);
            if(!UserChannels.ContainsKey(userId))
                UserChannels.Add(userId, []);

            channelUsers.Add(userId);
            RecordChannelUserActivity(channelId, userId);
        }
    }

    public void RemoveChannelUser(Channel channel, User user)
        => RemoveChannelUser(channel.Id, user.UserId);
    public void RemoveChannelUser(long channelId, User user)
        => RemoveChannelUser(channelId, user.UserId);
    public void RemoveChannelUser(Channel channel, string userId)
        => RemoveChannelUser(channel.Id, userId);

    public void RemoveChannelUser(long channelId, string userId) {
        lock(@lock) {
            ChannelUsers.Remove(channelId);
            UserChannels.Remove(userId);
            UpdateUserLastChannel(userId, channelId);
        }
    }

    public bool HasChannelUser(Channel channel, User user)
        => HasChannelUser(channel.Id, user.UserId);
    public bool HasChannelUser(long channelId, User user)
        => HasChannelUser(channelId, user.UserId);
    public bool HasChannelUser(Channel channel, string userId)
        => HasChannelUser(channel.Id, userId);

    public bool HasChannelUser(long channelId, string userId) {
        lock(@lock)
            return ChannelUsers.TryGetValue(channelId, out var channelUsers) && channelUsers.Contains(userId);
    }

    public long? GetUserLastChannelId(User user)
        => GetUserLastChannelId(user.UserId);

    public long? GetUserLastChannelId(string userId) {
        lock(@lock)
            return UserLastChannel.TryGetValue(userId, out long channelId) ? channelId : null;
    }

    public Channel? GetUserLastChannel(User user)
        => GetUserLastChannel(user.UserId);

    public Channel? GetUserLastChannel(string userId) {
        lock(@lock) {
            long? channelId = GetUserLastChannelId(userId);
            return channelId.HasValue ? channelsCtx.GetChannel(channelId.Value) : null;
        }
    }

    public IEnumerable<string> GetChannelUserIds(Channel channel)
        => GetChannelUserIds(channel.Id);

    public IEnumerable<string> GetChannelUserIds(long channelId) {
        lock(@lock)
            return [.. GetChannelUserIdsInternal(channelId)];
    }

    private HashSet<string> GetChannelUserIdsInternal(long channelId) {
        return ChannelUsers.TryGetValue(channelId, out var channelUsers) ? channelUsers : [];
    }

    public IEnumerable<User> GetChannelUsers(Channel channel) {
        lock(@lock)
            return usersCtx.GetUsers(GetChannelUserIdsInternal(channel.Id));
    }

    public IEnumerable<User> GetChannelUsers(long channelId) {
        lock(@lock)
            return usersCtx.GetUsers(GetChannelUserIdsInternal(channelId));
    }

    public IEnumerable<long> GetUserChannelIds(User user)
        => GetUserChannelIds(user.UserId);

    public IEnumerable<long> GetUserChannelIds(string userId) {
        lock(@lock)
            return [.. GetUserChannelIdsInternal(userId)];
    }

    private IEnumerable<long> GetUserChannelIdsInternal(string userId) {
        return UserChannels.TryGetValue(userId, out var userChannels) ? userChannels.Keys : [];
    }

    public IEnumerable<Channel> GetUserChannels(User user) {
        lock(@lock)
            return channelsCtx.GetChannels(GetUserChannelIdsInternal(user.UserId));
    }

    public IEnumerable<Channel> GetUserChannels(string userId) {
        lock(@lock)
            return channelsCtx.GetChannels(GetUserChannelIdsInternal(userId));
    }
}