private async Task CheckPage(HtmlDocument htmlDocument)
        {
            if (htmlDocument == null)
            {
                Bot.ArchiLogger.LogNullError(nameof(htmlDocument));
                return;
            }

            HtmlNodeCollection htmlNodes = htmlDocument.DocumentNode.SelectNodes("//div[@class='badge_row_inner']");

            if (htmlNodes == null)
            {
                // No eligible badges whatsoever
                return;
            }

            HashSet <Task> backgroundTasks = new HashSet <Task>();

            foreach (HtmlNode htmlNode in htmlNodes)
            {
                HtmlNode statsNode = htmlNode.SelectSingleNode(".//div[@class='badge_title_stats_content']");

                HtmlNode appIDNode = statsNode?.SelectSingleNode(".//div[@class='card_drop_info_dialog']");
                if (appIDNode == null)
                {
                    // It's just a badge, nothing more
                    continue;
                }

                string appIDText = appIDNode.GetAttributeValue("id", null);
                if (string.IsNullOrEmpty(appIDText))
                {
                    Bot.ArchiLogger.LogNullError(nameof(appIDText));
                    continue;
                }

                string[] appIDSplitted = appIDText.Split('_');
                if (appIDSplitted.Length < 5)
                {
                    Bot.ArchiLogger.LogNullError(nameof(appIDSplitted));
                    continue;
                }

                appIDText = appIDSplitted[4];

                if (!uint.TryParse(appIDText, out uint appID) || (appID == 0))
                {
                    Bot.ArchiLogger.LogNullError(nameof(appID));
                    continue;
                }

                if (GlobalConfig.SalesBlacklist.Contains(appID) || Program.GlobalConfig.Blacklist.Contains(appID) || Bot.IsBlacklistedFromIdling(appID) || (Bot.BotConfig.IdlePriorityQueueOnly && !Bot.IsPriorityIdling(appID)))
                {
                    // We're configured to ignore this appID, so skip it
                    continue;
                }

                if (IgnoredAppIDs.TryGetValue(appID, out DateTime ignoredUntil))
                {
                    if (ignoredUntil < DateTime.UtcNow)
                    {
                        // This game served its time as being ignored
                        IgnoredAppIDs.TryRemove(appID, out _);
                    }
                    else
                    {
                        // This game is still ignored
                        continue;
                    }
                }

                // Cards
                HtmlNode progressNode = statsNode.SelectSingleNode(".//span[@class='progress_info_bold']");
                if (progressNode == null)
                {
                    Bot.ArchiLogger.LogNullError(nameof(progressNode));
                    continue;
                }

                string progressText = progressNode.InnerText;
                if (string.IsNullOrEmpty(progressText))
                {
                    Bot.ArchiLogger.LogNullError(nameof(progressText));
                    continue;
                }

                ushort cardsRemaining = 0;
                Match  progressMatch  = Regex.Match(progressText, @"\d+");

                // This might fail if we have no card drops remaining, 0 is not printed in this case - that's fine
                if (progressMatch.Success)
                {
                    if (!ushort.TryParse(progressMatch.Value, out cardsRemaining) || (cardsRemaining == 0))
                    {
                        Bot.ArchiLogger.LogNullError(nameof(cardsRemaining));
                        continue;
                    }
                }

                if (cardsRemaining == 0)
                {
                    // Normally we'd trust this information and simply skip the rest
                    // However, Steam is so f****d up that we can't simply assume that it's correct
                    // It's entirely possible that actual game page has different info, and badge page lied to us
                    // We can't check every single game though, as this will literally kill people with cards from games they don't own
                    // Luckily for us, it seems to happen only with some specific games
                    if (!UntrustedAppIDs.Contains(appID))
                    {
                        continue;
                    }

                    // To save us on extra work, check cards earned so far first
                    HtmlNode cardsEarnedNode = statsNode.SelectSingleNode(".//div[@class='card_drop_info_header']");
                    if (cardsEarnedNode == null)
                    {
                        Bot.ArchiLogger.LogNullError(nameof(cardsEarnedNode));
                        continue;
                    }

                    string cardsEarnedText = cardsEarnedNode.InnerText;
                    if (string.IsNullOrEmpty(cardsEarnedText))
                    {
                        Bot.ArchiLogger.LogNullError(nameof(cardsEarnedText));
                        continue;
                    }

                    Match cardsEarnedMatch = Regex.Match(cardsEarnedText, @"\d+");
                    if (!cardsEarnedMatch.Success)
                    {
                        Bot.ArchiLogger.LogNullError(nameof(cardsEarnedMatch));
                        continue;
                    }

                    if (!ushort.TryParse(cardsEarnedMatch.Value, out ushort cardsEarned))
                    {
                        Bot.ArchiLogger.LogNullError(nameof(cardsEarned));
                        continue;
                    }

                    if (cardsEarned > 0)
                    {
                        // If we already earned some cards for this game, it's very likely that it's done
                        // Let's hope that trusting cardsRemaining AND cardsEarned is enough
                        // If I ever hear that it's not, I'll most likely need a doctor
                        continue;
                    }

                    // If we have no cardsRemaining and no cardsEarned, it's either:
                    // - A game we don't own physically, but we have cards from it in inventory
                    // - F2P game that we didn't spend any money in, but we have cards from it in inventory
                    // - Steam fuckup
                    // As you can guess, we must follow the rest of the logic in case of Steam fuckup
                    // Please kill me ;_;
                }

                // Hours
                HtmlNode timeNode = statsNode.SelectSingleNode(".//div[@class='badge_title_stats_playtime']");
                if (timeNode == null)
                {
                    Bot.ArchiLogger.LogNullError(nameof(timeNode));
                    continue;
                }

                string hoursText = timeNode.InnerText;
                if (string.IsNullOrEmpty(hoursText))
                {
                    Bot.ArchiLogger.LogNullError(nameof(hoursText));
                    continue;
                }

                float hours      = 0.0F;
                Match hoursMatch = Regex.Match(hoursText, @"[0-9\.,]+");

                // This might fail if we have exactly 0.0 hours played, as it's not printed in that case - that's fine
                if (hoursMatch.Success)
                {
                    if (!float.TryParse(hoursMatch.Value, NumberStyles.Number, CultureInfo.InvariantCulture, out hours) || (hours <= 0.0F))
                    {
                        Bot.ArchiLogger.LogNullError(nameof(hours));
                        continue;
                    }
                }

                // Names
                HtmlNode nameNode = statsNode.SelectSingleNode("(.//div[@class='card_drop_info_body'])[last()]");
                if (nameNode == null)
                {
                    Bot.ArchiLogger.LogNullError(nameof(nameNode));
                    continue;
                }

                string name = nameNode.InnerText;
                if (string.IsNullOrEmpty(name))
                {
                    Bot.ArchiLogger.LogNullError(nameof(name));
                    continue;
                }

                // We handle two cases here - normal one, and no card drops remaining
                int nameStartIndex = name.IndexOf(" by playing ", StringComparison.Ordinal);
                if (nameStartIndex <= 0)
                {
                    nameStartIndex = name.IndexOf("You don't have any more drops remaining for ", StringComparison.Ordinal);
                    if (nameStartIndex <= 0)
                    {
                        Bot.ArchiLogger.LogNullError(nameof(nameStartIndex));
                        continue;
                    }

                    nameStartIndex += 32;                     // + 12 below
                }

                nameStartIndex += 12;

                int nameEndIndex = name.LastIndexOf('.');
                if (nameEndIndex <= nameStartIndex)
                {
                    Bot.ArchiLogger.LogNullError(nameof(nameEndIndex));
                    continue;
                }

                name = WebUtility.HtmlDecode(name.Substring(nameStartIndex, nameEndIndex - nameStartIndex));

                // Levels
                byte badgeLevel = 0;

                HtmlNode levelNode = htmlNode.SelectSingleNode(".//div[@class='badge_info_description']/div[2]");
                if (levelNode != null)
                {
                    // There is no levelNode if we didn't craft that badge yet (level 0)
                    string levelText = levelNode.InnerText;
                    if (string.IsNullOrEmpty(levelText))
                    {
                        Bot.ArchiLogger.LogNullError(nameof(levelText));
                        continue;
                    }

                    int levelIndex = levelText.IndexOf("Level ", StringComparison.OrdinalIgnoreCase);
                    if (levelIndex < 0)
                    {
                        Bot.ArchiLogger.LogNullError(nameof(levelIndex));
                        continue;
                    }

                    levelIndex += 6;
                    if (levelText.Length <= levelIndex)
                    {
                        Bot.ArchiLogger.LogNullError(nameof(levelIndex));
                        continue;
                    }

                    levelText = levelText.Substring(levelIndex, 1);
                    if (!byte.TryParse(levelText, out badgeLevel) || (badgeLevel == 0) || (badgeLevel > 5))
                    {
                        Bot.ArchiLogger.LogNullError(nameof(badgeLevel));
                        continue;
                    }
                }

                // Done with parsing, we have two possible cases here
                // Either we have decent info about appID, name, hours, cardsRemaining (cardsRemaining > 0) and level
                // OR we strongly believe that Steam lied to us, in this case we will need to check game invidually (cardsRemaining == 0)
                if (cardsRemaining > 0)
                {
                    GamesToFarm.Add(new Game(appID, name, hours, cardsRemaining, badgeLevel));
                }
                else
                {
                    Task task = CheckGame(appID, name, hours, badgeLevel);
                    switch (Program.GlobalConfig.OptimizationMode)
                    {
                    case GlobalConfig.EOptimizationMode.MinMemoryUsage:
                        await task.ConfigureAwait(false);

                        break;

                    default:
                        backgroundTasks.Add(task);
                        break;
                    }
                }
            }

            // If we have any background tasks, wait for them
            if (backgroundTasks.Count > 0)
            {
                await Task.WhenAll(backgroundTasks).ConfigureAwait(false);
            }
        }