using Microsoft.Extensions.Logging;
using SharpChat.Auth;
using SharpChat.Bans;
using SharpChat.Channels;
using SharpChat.Configuration;
using SharpChat.Connections;
using SharpChat.Events;
using SharpChat.Messages;
using SharpChat.Sessions;
using SharpChat.Snowflake;
using SharpChat.SockChat;
using SharpChat.SockChat.S2CPackets;
using SharpChat.Storage;
using SharpChat.Users;
using System.Dynamic;
using System.Net;
using ZLogger;

namespace SharpChat;

public class Context {
    public const int DEFAULT_MSG_LENGTH_MAX = 5000;
    public const int DEFAULT_MAX_CONNECTIONS = 5;
    public const int DEFAULT_FLOOD_KICK_LENGTH = 30;
    public const int DEFAULT_FLOOD_KICK_EXEMPT_RANK = 9;

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

    public ILoggerFactory LoggerFactory { get; }
    public Config Config { get; }
    public MessageStorage Messages { get; }
    public AuthClient Auth { get; }
    public BansClient Bans { get; }

    public CachedValue<int> MaxMessageLength { get; }
    public CachedValue<int> MaxConnections { get; }
    public CachedValue<int> FloodKickLength { get; }
    public CachedValue<int> FloodKickExemptRank { get; }

    private readonly ILogger Logger;

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

    public UsersContext Users { get; } = new();
    public SessionsContext Sessions { get; }
    public ChannelsContext Channels { get; }
    public ChannelsUsersContext ChannelsUsers { get; }
    public Dictionary<string, RateLimiter> UserRateLimiters { get; } = [];

    public Context(
        ILoggerFactory loggerFactory,
        Config config,
        StorageBackend storage,
        AuthClient authClient,
        BansClient bansClient
    ) {
        LoggerFactory = loggerFactory;
        Logger = loggerFactory.CreateLogger("ctx");
        Config = config;
        Messages = storage.CreateMessageStorage();
        Auth = authClient;
        Bans = bansClient;

        RandomSnowflake = new(SnowflakeGenerator);
        Sessions = new(loggerFactory, RandomSnowflake);
        Channels = new(RandomSnowflake);
        ChannelsUsers = new(Channels, Users);

        Logger.ZLogDebug($"Reading cached config values...");
        MaxMessageLength = config.ReadCached("msgMaxLength", DEFAULT_MSG_LENGTH_MAX);
        MaxConnections = config.ReadCached("connMaxCount", DEFAULT_MAX_CONNECTIONS);
        FloodKickLength = config.ReadCached("floodKickLength", DEFAULT_FLOOD_KICK_LENGTH);
        FloodKickExemptRank = config.ReadCached("floodKickExemptRank", DEFAULT_FLOOD_KICK_EXEMPT_RANK);

        Logger.ZLogDebug($"Creating channel list...");
        string[] channelNames = config.ReadValue<string[]>("channels") ?? ["lounge"];
        if(channelNames is not null)
            foreach(string channelName in channelNames) {
                Config channelCfg = config.ScopeTo($"channels:{channelName}");

                string name = channelCfg.SafeReadValue("name", string.Empty)!;
                if(string.IsNullOrWhiteSpace(name))
                    name = channelName;

                Channels.CreateChannel(
                    name,
                    channelCfg.SafeReadValue("password", string.Empty)!,
                    rank: channelCfg.SafeReadValue("minRank", 0)
                );
            }
    }

    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.GetUsers(uids);
                User? target = users.FirstOrDefault(u => mce.SenderId.Equals(u.UserId, StringComparison.Ordinal));
                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.GetLegacyName()} {mce.MessageText}" : mce.MessageText,
                        mce.IsAction,
                        true
                    ));
            } else {
                Channel? channel = Channels.GetChannel(mce.ChannelName);
                if(channel is not null)
                    await SendTo(channel, new ChatMessageAddS2CPacket(
                        mce.MessageId,
                        DateTimeOffset.Now,
                        mce.SenderId,
                        mce.MessageText,
                        mce.IsAction,
                        false
                    ));
            }

            dynamic data = new ExpandoObject();
            data.text = mce.MessageText;
            if(mce.IsAction)
                data.act = true;

            await Messages.LogMessage(mce.MessageId, "msg:add", mce.ChannelName, mce.SenderId, mce.SenderName, mce.SenderColour, mce.SenderRank, mce.SenderNickName, mce.SenderPerms, data);
            return;
        }
    }

    public async Task Update() {
        foreach(Session session in Sessions.GetTimedOutSessions()) {
            session.Logger.ZLogInformation($"Nuking connection associated with user #{session.UserId}");
            session.Connection.Close(ConnectionCloseReason.TimeOut);
            Sessions.DestroySession(session);
        }

        foreach(User user in Users.GetUsers())
            if(Sessions.CountActiveSessions(user) < 1) {
                Logger.ZLogInformation($"Timing out user {user.UserId} (no more connections).");
                await HandleDisconnect(user, UserDisconnectS2CPacket.Reason.TimeOut);
            }
    }

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

    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
    ) {
        string previousName = user.GetLegacyName();
        UserDiff diff = Users.UpdateUser(
            user,
            userName,
            colour,
            rank,
            perms,
            nickName,
            status,
            statusText
        );

        if(diff.Changed) {
            string currentName = user.GetLegacyNameWithStatus();

            if(!silent && diff.Nick.Changed)
                await SendToUserChannels(user, new CommandResponseS2CPacket(RandomSnowflake.Next(), LCR.NICKNAME_CHANGE, false, previousName, currentName));

            await SendToUserChannels(user, new UserUpdateS2CPacket(diff.Id, currentName, diff.Colour.After, diff.Rank.After, diff.Permissions.After));
        }
    }

    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(SockChatConnection conn in Sessions.GetConnections<SockChatConnection>(user)) {
            conn.Close(ConnectionCloseReason.Unauthorized);
            Sessions.DestroySession(conn);
        }

        await Update();
        await HandleDisconnect(user, reason);
    }

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

        foreach(Channel chan in ChannelsUsers.GetUserChannels(user)) {
            ChannelsUsers.RemoveChannelUser(chan, user);

            long msgId = RandomSnowflake.Next();
            await SendTo(chan, new UserDisconnectS2CPacket(msgId, DateTimeOffset.Now, user.UserId, user.GetLegacyNameWithStatus(), reason));
            await Messages.LogMessage(msgId, "user:disconnect", chan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, new { reason = (int)reason });

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

        ChannelsUsers.RemoveUser(user);
    }

    public async Task SwitchChannel(User user, Channel chan, string password) {
        Channel? oldChan = ChannelsUsers.GetUserLastChannel(user);

        if(oldChan?.Id == chan.Id) {
            await ForceChannel(user);
            return;
        }

        if(!user.Permissions.HasFlag(UserPermissions.JoinAnyChannel) && chan.IsOwner(user.UserId)) {
            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.SlowUtf8Equals(password)) {
                await SendTo(user, new CommandResponseS2CPacket(RandomSnowflake.Next(), LCR.CHANNEL_INVALID_PASSWORD, true, chan.Name));
                await ForceChannel(user);
                return;
            }
        }

        if(oldChan is not null) {
            long leaveId = RandomSnowflake.Next();
            await SendTo(oldChan, new UserChannelLeaveS2CPacket(leaveId, user.UserId));
            await Messages.LogMessage(leaveId, "chan:leave", oldChan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions);
            ChannelsUsers.RemoveChannelUser(oldChan, user);

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

        long joinId = RandomSnowflake.Next();
        await SendTo(chan, new UserChannelJoinS2CPacket(joinId, user.UserId, user.GetLegacyNameWithStatus(), user.Colour, user.Rank, user.Permissions));
        await Messages.LogMessage(joinId, "chan:join", chan.Name, user.UserId, user.GetLegacyName(), user.Colour, user.Rank, user.NickName, user.Permissions);

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

        IEnumerable<Message> msgs = await Messages.GetMessages(chan.Name);
        foreach(Message msg in msgs)
            await SendTo(user, new ContextMessageS2CPacket(msg));

        await ForceChannel(user, chan);
        ChannelsUsers.AddChannelUser(chan, user);
    }

    public async Task Send(S2CPacket packet) {
        foreach(SockChatConnection conn in Sessions.GetConnections<SockChatConnection>())
            await conn.Send(packet);
    }

    public async Task SendTo(User user, S2CPacket packet) {
        foreach(SockChatConnection conn in Sessions.GetConnections<SockChatConnection>(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<SockChatConnection> conns = Sessions.GetConnections<SockChatConnection>(
            s => ChannelsUsers.HasChannelUser(channel, s.UserId)
        );
        foreach(SockChatConnection conn in conns)
            await conn.Send(packet);
    }

    public async Task SendToUserChannels(User user, S2CPacket packet) {
        IEnumerable<Channel> chans = ChannelsUsers.GetUserChannels(user);
        IEnumerable<SockChatConnection> conns = Sessions.GetConnections<SockChatConnection>(
            s => chans.Any(c => ChannelsUsers.HasChannelUser(c.Id, s.UserId))
        );
        foreach(SockChatConnection conn in conns)
            await conn.Send(packet);
    }

    public async Task ForceChannel(User user, Channel? chan = null) {
        chan ??= ChannelsUsers.GetUserLastChannel(user) ?? throw new ArgumentException("no channel???");
        await SendTo(user, new UserChannelForceJoinS2CPacket(chan.Name));
    }

    public async Task UpdateChannel(
        Channel channel,
        bool? temporary = null,
        int? rank = null,
        string? password = null
    ) {
        ChannelDiff diff = Channels.UpdateChannel(
            channel,
            temporary: temporary,
            rank: rank,
            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
        if(diff.Changed)
            foreach(User user in Users.GetUsersOfMinimumRank(channel.Rank))
                await SendTo(user, new ChannelUpdateS2CPacket(channel.Name, channel.Name, channel.HasPassword, channel.IsTemporary));
    }

    public async Task RemoveChannel(Channel channel) {
        // Remove channel from the listing
        Channels.RemoveChannel(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 ChannelsUsers.GetChannelUsers(channel))
            await SwitchChannel(user, Channels.GetDefaultChannel(), string.Empty);

        // Broadcast deletion of channel
        foreach(User user in Users.GetUsersOfMinimumRank(channel.Rank))
            await SendTo(user, new ChannelDeleteS2CPacket(channel.Name));

        ChannelsUsers.RemoveChannel(channel);
    }
}