using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models;

namespace EllieBot.Modules.Games.Quests;

public sealed class QuestService(
    DbService db,
    IBotCache botCache,
    IMessageSenderService sender,
    DiscordSocketClient client
) : IEService, IExecPreCommand
{
    private readonly IQuest[] _availableQuests =
    [
        new HangmanWinQuest(),
        new PlantPickQuest(),
        new BetQuest(),
        new BetFlowersQuest(),
        new GiftWaifuQuest(),
        new CatchFishQuest(),
        new SetPixelsQuest(),
        new JoinAnimalRaceQuest(),
        new BankerQuest(),
        new CheckLeaderboardsQuest(),
        new WellInformedQuest(),
    ];

    private const int MAX_QUESTS_PER_DAY = 3;

    private TypedKey<bool> UserHasQuestsKey(ulong userId)
        => new($"daily:generated:{userId}");

    private TypedKey<bool> UserCompletedDailiesKey(ulong userId)
        => new($"daily:completed:{userId}");


    public Task ReportActionAsync(
        ulong userId,
        QuestEventType eventType,
        Dictionary<string, string>? metadata = null)
    {
        // don't block any caller

        _ = Task.Run(async () =>
        {
            metadata ??= new();
            var now = DateTime.UtcNow;
            
            var alreadyDone = await botCache.GetAsync(UserCompletedDailiesKey(userId));
            if (alreadyDone.IsT0)
                return;

            var userQuests = await GetUserQuestsAsync(userId, now);
            
            foreach (var (q, uq) in userQuests)
            {
                // deleted quest
                if (q is null)
                    continue;

                // user already completed or incorrect event
                if (uq.IsCompleted || q.EventType != eventType)
                    continue;

                var newProgress = q.TryUpdateProgress(metadata, uq.Progress);

                // user already did that part of the quest
                if (newProgress == uq.Progress)
                    continue;

                var isCompleted = newProgress >= q.RequiredAmount;

                await using var uow = db.GetDbContext();
                await uow.GetTable<UserQuest>()
                    .Where(x => x.UserId == userId && x.QuestId == q.QuestId && x.QuestNumber == uq.QuestNumber)
                    .Set(x => x.Progress, newProgress)
                    .Set(x => x.IsCompleted, isCompleted)
                    .UpdateAsync();

                uq.IsCompleted = isCompleted;

                if (userQuests.All(x => x.UserQuest.IsCompleted))
                {
                    var timeUntilTomorrow = now.Date.AddDays(1) - DateTime.UtcNow;
                    if (!await botCache.AddAsync(
                            UserCompletedDailiesKey(userId),
                            true,
                            expiry: timeUntilTomorrow))
                        return;

                    try
                    {
                        var user = await client.GetUserAsync(userId);
                        await sender
                            .Response(user)
                            .Confirm(strs.dailies_done)
                            .SendAsync();
                    }
                    catch
                    {
                        // we don't really care if the user receives it
                    }

                    break;
                }
            }
        });

        return Task.CompletedTask;
    }

    public async Task<IReadOnlyList<(IQuest? Quest, UserQuest UserQuest)>> GetUserQuestsAsync(
        ulong userId,
        DateTime now)
    {
        var today = now.Date;
        await EnsureUserDailiesAsync(userId, today);

        await using var uow = db.GetDbContext();
        var quests = await uow.GetTable<UserQuest>()
            .Where(x => x.UserId == userId && x.DateAssigned == today)
            .ToListAsync();

        return quests
            .Select(x => (_availableQuests.FirstOrDefault(q => q.QuestId == x.QuestId), x))
            .Select(x => x!)
            .ToList();
    }

    private async Task EnsureUserDailiesAsync(ulong userId, DateTime date)
    {
        var today = date.Date;
        var timeUntilTomorrow = today.AddDays(1) - DateTime.UtcNow;
        if (!await botCache.AddAsync(UserHasQuestsKey(userId), true, expiry: timeUntilTomorrow, overwrite: false))
            return;

        await using var uow = db.GetDbContext();
        var newQuests = GenerateDailyQuestsAsync();
        for (var i = 0; i < MAX_QUESTS_PER_DAY; i++)
        {
            await uow.GetTable<UserQuest>()
                .InsertOrUpdateAsync(() => new()
                    {
                        UserId = userId,
                        QuestNumber = i,
                        DateAssigned = today,

                        IsCompleted = false,
                        QuestId = newQuests[i].QuestId,
                        Progress = 0,
                    },
                    old => new()
                    {
                    },
                    () => new()
                    {
                        UserId = userId,
                        QuestNumber = i,
                        DateAssigned = today
                    });
        }
    }

    private IReadOnlyList<IQuest> GenerateDailyQuestsAsync()
    {
        return _availableQuests
            .ToList()
            .Shuffle()
            .Take(MAX_QUESTS_PER_DAY)
            .ToList();
    }

    public int Priority
        => int.MinValue;

    public async Task<bool> ExecPreCommandAsync(ICommandContext context, string moduleName, CommandInfo command)
    {
        var cmdName = command.Name.ToLowerInvariant();

        await ReportActionAsync(
            context.User.Id,
            QuestEventType.CommandUsed,
            new()
            {
                { "name", cmdName }
            });

        return false;
    }

    public async Task<bool> UserCompletedDailies(ulong userId)
    {
        var result = await botCache.GetAsync(UserCompletedDailiesKey(userId));

        return result.IsT0;
    }
}