using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models;
using EllieBot.Generators;
using EllieBot.Modules.Administration.Services;
using EllieBot.Modules.Gambling;
using EllieBot.Modules.Xp.Services;

namespace EllieBot.Modules.Administration;

public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
{
    private readonly DbService _db;
    private readonly IMessageSenderService _mss;
    private readonly DiscordSocketClient _client;
    private readonly IBotCreds _creds;
    private readonly IReplacementService _repSvc;
    private readonly IPubSub _pubSub;

    private ConcurrentDictionary<NotifyType, ConcurrentDictionary<ulong, Notify>> _events = new();

    public NotifyService(
        DbService db,
        IMessageSenderService mss,
        DiscordSocketClient client,
        IBotCreds creds,
        IReplacementService repSvc,
        IPubSub pubSub)
    {
        _db = db;
        _mss = mss;
        _client = client;
        _creds = creds;
        _repSvc = repSvc;
        _pubSub = pubSub;
    }

    private void RegisterModels()
    {
        RegisterModel<LevelUpNotifyModel>();
        RegisterModel<ProtectionNotifyModel>();
        RegisterModel<AddRoleRewardNotifyModel>();
        RegisterModel<RemoveRoleRewardNotifyModel>();
        RegisterModel<NiceCatchNotifyModel>();
    }

    public async Task OnReadyAsync()
    {
        await using var uow = _db.GetDbContext();
        _events = (await uow.GetTable<Notify>()
                .Where(x => Queries.GuildOnShard(x.GuildId,
                    _creds.TotalShards,
                    _client.ShardId))
                .ToListAsyncLinqToDB())
            .GroupBy(x => x.Type)
            .ToDictionary(x => x.Key, x => x.ToDictionary(x => x.GuildId).ToConcurrent())
            .ToConcurrent();
        
        RegisterModels();
    }

    private async Task SubscribeToEvent<T>()
        where T : struct, INotifyModel<T>
    {
        await _pubSub.Sub(new TypedKey<T>(T.KeyName), async (model) => await OnEvent(model));
    }

    public async Task NotifyAsync<T>(T data, bool isShardLocal = false)
        where T : struct, INotifyModel<T>
    {
        try
        {
            if (isShardLocal)
            {
                _ = Task.Run(async () => await OnEvent(data));
                return;
            }

            await _pubSub.Pub(data.GetTypedKey(), data);
        }
        catch (Exception ex)
        {
            Log.Warning(ex,
                "Unknown error occurred while trying to trigger {NotifyEvent} for {NotifyModel}",
                T.KeyName,
                data);
        }
    }

    private async Task OnEvent<T>(T model)
        where T : struct, INotifyModel<T>
    {
        if (!_events.TryGetValue(T.NotifyType, out var subs))
            return;

        // make sure the event is consumed
        // only in the guild it was meant for
        if (model.TryGetGuildId(out var gid))
        {
            if (!subs.TryGetValue(gid, out var conf))
                return;

            await HandleNotifyEvent(conf, model);
            return;
        }

        // todo optimize this
        foreach (var key in subs.Keys)
        {
            if (subs.TryGetValue(key, out var notif))
            {
                try
                {
                    await HandleNotifyEvent(notif, model);
                }
                catch (Exception ex)
                {
                    Log.Error(ex,
                        "Error occured while sending notification {NotifyEvent} to guild {GuildId}: {ErrorMessage}",
                        T.NotifyType,
                        key,
                        ex.Message);
                }

                await Task.Delay(500);
            }
        }
    }

    private async Task HandleNotifyEvent<T>(Notify conf, T model)
        where T : struct, INotifyModel<T>
    {
        var guild = _client.GetGuild(conf.GuildId);

        // bot probably left the guild, cleanup?
        if (guild is null)
            return;

        IMessageChannel? channel;
        // if notify channel is specified for this event, send the event to that channel
        if (conf.ChannelId is ulong confCid)
        {
            channel = guild.GetTextChannel(confCid);
        }
        else
        {
            // otherwise get the origin channel of the event
            if (!model.TryGetChannelId(out var cid))
                return;

            channel = guild.GetChannel(cid) as IMessageChannel;
        }

        if (channel is null)
            return;

        IUser? user = null;
        if (model.TryGetUserId(out var userId))
        {
            user = guild.GetUser(userId) ?? _client.GetUser(userId);
        }

        var rctx = new ReplacementContext(guild: guild, channel: channel, user: user);

        var st = SmartText.CreateFrom(conf.Message);
        foreach (var modelRep in T.GetReplacements())
        {
            rctx.WithOverride(GetPhToken(modelRep.Name), () => modelRep.Func(model, guild));
        }

        st = await _repSvc.ReplaceAsync(st, rctx);
        if (st is SmartPlainText spt)
        {
            await _mss.Response(channel)
                .Confirm(spt.Text)
                .SendAsync();
            return;
        }

        await _mss.Response(channel)
            .Text(st)
            .Sanitize(false)
            .SendAsync();
    }

    private static string GetPhToken(string name)
        => $"%event.{name}%";

    public async Task<bool> EnableAsync(
        ulong guildId,
        ulong? channelId,
        NotifyType nType,
        string message)
    {
        // check if the notify type model supports null channel
        if (channelId is null)
        {
            var model = GetRegisteredModel(nType);
            if (!model.SupportsOriginTarget)
                return false;
        }


        await using var uow = _db.GetDbContext();
        await uow.GetTable<Notify>()
            .InsertOrUpdateAsync(() => new()
            {
                GuildId = guildId,
                ChannelId = channelId,
                Type = nType,
                Message = message,
            },
                (_) => new()
                {
                    Message = message,
                    ChannelId = channelId
                },
                () => new()
                {
                    GuildId = guildId,
                    Type = nType
                });

        var eventDict = _events.GetOrAdd(nType, _ => new());
        eventDict[guildId] = new()
        {
            GuildId = guildId,
            ChannelId = channelId,
            Type = nType,
            Message = message
        };

        return true;
    }

    public async Task DisableAsync(ulong guildId, NotifyType nType)
    {
        await using var uow = _db.GetDbContext();
        var deleted = await uow.GetTable<Notify>()
            .Where(x => x.GuildId == guildId && x.Type == nType)
            .DeleteAsync();

        if (deleted == 0)
            return;

        if (!_events.TryGetValue(nType, out var guildsDict))
            return;

        guildsDict.TryRemove(guildId, out _);
    }

    public async Task<IReadOnlyCollection<Notify>> GetForGuildAsync(ulong guildId, int page = 0)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(page);

        await using var ctx = _db.GetDbContext();
        var list = await ctx.GetTable<Notify>()
            .Where(x => x.GuildId == guildId)
            .OrderBy(x => x.Type)
            .Skip(page * 10)
            .Take(10)
            .ToListAsyncLinqToDB();

        return list;
    }

    public async Task<Notify?> GetNotifyAsync(ulong guildId, NotifyType nType)
    {
        await using var ctx = _db.GetDbContext();
        return await ctx.GetTable<Notify>()
            .Where(x => x.GuildId == guildId && x.Type == nType)
            .FirstOrDefaultAsyncLinqToDB();
    }

    // messed up big time, it was supposed to be fully extensible, but it's stored as an enum in the database already...
    private readonly ConcurrentDictionary<NotifyType, NotifyModelData> _models = new();

    public void RegisterModel<T>() where T : struct, INotifyModel<T>
    {
        var data = new NotifyModelData(T.NotifyType,
            T.SupportsOriginTarget,
            T.GetReplacements().Map(x => x.Name));
        
        _models[T.NotifyType] = data;

        _pubSub.Sub<T>(new(T.KeyName), async (data) => await OnEvent(data));
    }

    public NotifyModelData GetRegisteredModel(NotifyType nType)
        => _models[nType];
}