using Core.Helpers;
using SptCommon.Annotations;
using Core.Models.Eft.Common;
using Core.Models.Eft.Common.Tables;
using Core.Models.Eft.Hideout;
using Core.Models.Eft.ItemEvent;
using Core.Models.Eft.Profile;
using Core.Models.Enums;
using Core.Models.Enums.Hideout;
using Core.Models.Spt.Config;
using Core.Models.Spt.Hideout;
using Core.Models.Utils;
using Core.Routers;
using Core.Servers;
using Core.Utils;
using Core.Utils.Cloners;
using SptCommon.Extensions;
using LogLevel = Core.Models.Spt.Logging.LogLevel;

namespace Core.Services;

[Injectable(InjectionType.Singleton)]
public class CircleOfCultistService(
    ISptLogger<CircleOfCultistService> _logger,
    TimeUtil _timeUtil,
    ICloner _cloner,
    EventOutputHolder _eventOutputHolder,
    RandomUtil _randomUtil,
    HashUtil _hashUtil,
    ItemHelper _itemHelper,
    PresetHelper _presetHelper,
    ProfileHelper _profileHelper,
    InventoryHelper _inventoryHelper,
    HideoutHelper _hideoutHelper,
    QuestHelper _questHelper,
    DatabaseService _databaseService,
    ItemFilterService _itemFilterService,
    SeasonalEventService _seasonalEventService,
    ConfigServer _configServer
)
{
    protected HideoutConfig _hideoutConfig = _configServer.GetConfig<HideoutConfig>();
    public const string CircleOfCultistSlotId = "CircleOfCultistsGrid1";

    /// <summary>
    /// Start a sacrifice event
    /// Generate rewards
    /// Delete sacrificed items
    /// </summary>
    /// <param name="sessionId">Session id</param>
    /// <param name="pmcData">Player profile doing sacrifice</param>
    /// <param name="request">Client request</param>
    /// <returns>ItemEventRouterResponse</returns>
    public ItemEventRouterResponse StartSacrifice(
        string sessionId,
        PmcData pmcData,
        HideoutCircleOfCultistProductionStartRequestData request
    )
    {
        var cultistCircleStashId = pmcData.Inventory.HideoutAreaStashes.GetValueOrDefault(HideoutAreas.CIRCLE_OF_CULTISTS.ToString());

        // `cultistRecipes` just has single recipeId
        var cultistCraftData = _databaseService.GetHideout().Production.CultistRecipes.FirstOrDefault();
        List<Item> sacrificedItems = GetSacrificedItems(pmcData);
        var sacrificedItemCostRoubles = sacrificedItems.Aggregate(
            0D,
            (sum, curr) => sum + (_itemHelper.GetItemPrice(curr.Template) ?? 0)
        );

        var rewardAmountMultiplier = GetRewardAmountMultiplier(pmcData, _hideoutConfig.CultistCircle);

        // Get the rouble amount we generate rewards with from cost of sacrified items * above multipler
        var rewardAmountRoubles = Math.Round(sacrificedItemCostRoubles * rewardAmountMultiplier);

        // Check if it matches any direct swap recipes
        var directRewardsCache = GenerateSacrificedItemsCache(_hideoutConfig.CultistCircle.DirectRewards);
        var directRewardSettings = CheckForDirectReward(sessionId, sacrificedItems, directRewardsCache);
        var hasDirectReward = directRewardSettings?.Reward.Count > 0;

        // Get craft time and bonus status
        var craftingInfo = GetCircleCraftingInfo(
            rewardAmountRoubles,
            _hideoutConfig.CultistCircle,
            directRewardSettings
        );

        // Create production in pmc profile
        RegisterCircleOfCultistProduction(
            sessionId,
            pmcData,
            cultistCraftData.Id,
            sacrificedItems,
            craftingInfo.Time
        );

        var output = _eventOutputHolder.GetOutput(sessionId);

        // Remove sacrificed items from circle inventory
        foreach (var item in sacrificedItems)
        {
            if (item.SlotId == CircleOfCultistService.CircleOfCultistSlotId)
            {
                _inventoryHelper.RemoveItem(pmcData, item.Id, sessionId, output);
            }
        }

        var rewards = hasDirectReward
            ? GetDirectRewards(sessionId, directRewardSettings, cultistCircleStashId)
            : GetRewardsWithinBudget(
                GetCultistCircleRewardPool(sessionId, pmcData, craftingInfo, _hideoutConfig.CultistCircle),
                rewardAmountRoubles,
                cultistCircleStashId,
                _hideoutConfig.CultistCircle
            );

        // Get the container grid for cultist stash area
        var cultistStashDbItem = _itemHelper.GetItem(ItemTpl.HIDEOUTAREACONTAINER_CIRCLEOFCULTISTS_STASH_1);

        // Ensure rewards fit into container
        var containerGrid = _inventoryHelper.GetContainerSlotMap(cultistStashDbItem.Value.Id);
        AddRewardsToCircleContainer(sessionId, pmcData, rewards, containerGrid, cultistCircleStashId, output);

        return output;
    }

    private double GetRewardAmountMultiplier(PmcData pmcData, CultistCircleSettings cultistCircleSettings)
    {
        // Get a randomised value to multiply the sacrificed rouble cost by
        var rewardAmountMultiplier = _randomUtil.GetDouble(
            (double)cultistCircleSettings.RewardPriceMultiplerMinMax.Min,
            (double)cultistCircleSettings.RewardPriceMultiplerMinMax.Max
        );

        // Adjust value generated by the players hideout management skill
        var hideoutManagementSkill = _profileHelper.GetSkillFromProfile(pmcData, SkillTypes.HideoutManagement);
        if (hideoutManagementSkill is not null)
        {
            rewardAmountMultiplier *=
                (float)(1 + hideoutManagementSkill.Progress / 10000); // 5100 becomes 0.51, add 1 to it, 1.51, multiply the bonus by it (e.g. 1.2 x 1.51)
        }

        return rewardAmountMultiplier;
    }

    /// <summary>
    /// Register production inside player profile
    /// </summary>
    /// <param name="sessionId">Session id</param>
    /// <param name="pmcData">Player profile</param>
    /// <param name="recipeId">Recipe id</param>
    /// <param name="sacrificedItems">Items player sacrificed</param>
    /// <param name="craftingTime">How long the ritual should take</param>
    protected void RegisterCircleOfCultistProduction(
        string sessionId,
        PmcData pmcData,
        string recipeId,
        List<Item> sacrificedItems,
        double craftingTime
    )
    {
        // Create circle production/craft object to add to player profile
        var cultistProduction = _hideoutHelper.InitProduction(recipeId, craftingTime, false);

        // Flag as cultist circle for code to pick up later
        cultistProduction.SptIsCultistCircle = true;

        // Add items player sacrificed
        cultistProduction.GivenItemsInStart = sacrificedItems;

        // Add circle production to profile keyed to recipe id
        pmcData.Hideout.Production[recipeId] = cultistProduction;
    }

    /// <summary>
    /// Get the circle craft time as seconds, value is based on reward item value
    /// And get the bonus status to determine what tier of reward is given
    /// </summary>
    /// <param name="rewardAmountRoubles">Value of rewards in roubles</param>
    /// <param name="circleConfig">Circle config values</param>
    /// <param name="directRewardSettings">OPTIONAL - Values related to direct reward being given</param>
    /// <returns>craft time + type of reward + reward details</returns>
    protected CircleCraftDetails GetCircleCraftingInfo(
        double rewardAmountRoubles,
        CultistCircleSettings circleConfig,
        DirectRewardSettings directRewardSettings = null
    )
    {
        var result = new CircleCraftDetails
        {
            Time = -1,
            RewardType = CircleRewardType.RANDOM,
            RewardAmountRoubles = (int)rewardAmountRoubles,
            RewardDetails = null,
        };

        // Direct reward edge case
        if (directRewardSettings is not null)
        {
            result.Time = directRewardSettings.CraftTimeSeconds;

            return result;
        }

        var random = new Random();

        // Get a threshold where sacrificed amount is between thresholds min and max
        var matchingThreshold = GetMatchingThreshold(circleConfig.CraftTimeThreshholds, rewardAmountRoubles);
        if (
            rewardAmountRoubles >= circleConfig.HideoutCraftSacrificeThresholdRub &&
            random.Next(0, 1) <= circleConfig.BonusChanceMultiplier
        )
        {
            // Sacrifice amount is enough + passed 25% check to get hideout/task rewards
            result.Time =
                circleConfig.CraftTimeOverride != -1
                    ? circleConfig.CraftTimeOverride
                    : circleConfig.HideoutTaskRewardTimeSeconds;
            result.RewardType = CircleRewardType.HIDEOUT_TASK;

            return result;
        }

        // Edge case, check if override exists, Otherwise use matching threshold craft time
        result.Time =
            circleConfig.CraftTimeOverride != -1 ? circleConfig.CraftTimeOverride : matchingThreshold.CraftTimeSeconds;

        result.RewardDetails = matchingThreshold;

        return result;
    }

    protected CraftTimeThreshold GetMatchingThreshold(
        List<CraftTimeThreshold> thresholds,
        double rewardAmountRoubles
    )
    {
        var matchingThreshold = thresholds.FirstOrDefault(
            (craftThreshold) => craftThreshold.Min <= rewardAmountRoubles && craftThreshold.Max >= rewardAmountRoubles
        );

        // No matching threshold, make one
        if (matchingThreshold is null)
        {
            // None found, use a defalt
            _logger.Warning("Unable to find a matching cultist circle threshold, using fallback of 12 hours");

            // Use first threshold value (cheapest) from parameter array, otherwise use 12 hours
            var firstThreshold = thresholds.FirstOrDefault();
            var craftTime = firstThreshold?.CraftTimeSeconds is not null && firstThreshold.CraftTimeSeconds > 0
                ? firstThreshold.CraftTimeSeconds
                : _timeUtil.GetHoursAsSeconds(12);

            return new CraftTimeThreshold { Min = firstThreshold?.Min ?? 1, Max = firstThreshold?.Max ?? 34999, CraftTimeSeconds = craftTime };
        }

        return matchingThreshold;
    }

    /// <summary>
    /// Get the items player sacrificed in circle
    /// </summary>
    /// <param name="pmcData">Player profile</param>
    /// <returns>Array of items from player inventory</returns>
    protected List<Item> GetSacrificedItems(PmcData pmcData)
    {
        // Get root items that are in the cultist sacrifice window
        var inventoryRootItemsInCultistGrid = pmcData.Inventory.Items.Where(
            (item) => item.SlotId == CircleOfCultistService.CircleOfCultistSlotId
        );

        // Get rootitem + its children
        List<Item> sacrificedItems = [];
        foreach (var rootItem in inventoryRootItemsInCultistGrid)
        {
            var rootItemWithChildren = _itemHelper.FindAndReturnChildrenAsItems(
                pmcData.Inventory.Items,
                rootItem.Id
            );
            sacrificedItems.AddRange(rootItemWithChildren);
        }

        return sacrificedItems;
    }

    /// <summary>
    /// Given a pool of items + rouble budget, pick items until the budget is reached
    /// </summary>
    /// <param name="rewardItemTplPool">Items that can be picked</param>
    /// <param name="rewardBudget">Rouble budget to reach</param>
    /// <param name="cultistCircleStashId">Id of stash item</param>
    /// <returns>Array of item arrays</returns>
    protected List<List<Item>> GetRewardsWithinBudget(
        List<string> rewardItemTplPool,
        double rewardBudget,
        string cultistCircleStashId,
        CultistCircleSettings circleConfig
    )
    {
        // Prep rewards array (reward can be item with children, hence array of arrays)
        List<List<Item>> rewards = [];

        // Pick random rewards until we have exhausted the sacrificed items budget
        var totalRewardCost = 0;
        var rewardItemCount = 0;
        var failedAttempts = 0;
        while (
            totalRewardCost < rewardBudget &&
            rewardItemTplPool.Count > 0 &&
            rewardItemCount < circleConfig.MaxRewardItemCount
        )
        {
            if (failedAttempts > circleConfig.MaxAttemptsToPickRewardsWithinBudget)
            {
                _logger.Warning($"Exiting reward generation after {failedAttempts} failed attempts");

                break;
            }

            // Choose a random tpl from pool
            var randomItemTplFromPool = _randomUtil.GetArrayValue(rewardItemTplPool);

            // Is weapon/armor, handle differently
            if (
                _itemHelper.ArmorItemHasRemovableOrSoftInsertSlots(randomItemTplFromPool) ||
                _itemHelper.IsOfBaseclass(randomItemTplFromPool, BaseClasses.WEAPON)
            )
            {
                var defaultPreset = _presetHelper.GetDefaultPreset(randomItemTplFromPool);
                if (defaultPreset is null)
                {
                    _logger.Warning($"Reward tpl: {randomItemTplFromPool} lacks a default preset, skipping reward");
                    failedAttempts++;

                    continue;
                }

                // Ensure preset has unique ids and is cloned so we don't alter the preset data stored in memory
                var presetAndMods = _itemHelper.ReplaceIDs(defaultPreset.Items);
                _itemHelper.RemapRootItemId(presetAndMods);

                rewardItemCount++;
                totalRewardCost += (int)_itemHelper.GetItemPrice(randomItemTplFromPool);
                rewards.Add(presetAndMods);

                continue;
            }

            // Some items can have variable stack size, e.g. ammo / currency
            var stackSize = GetRewardStackSize(
                randomItemTplFromPool,
                (int)(rewardBudget / (rewardItemCount == 0 ? 1 : rewardItemCount)) // Remaining rouble budget
            );

            // Not a weapon/armor, standard single item
            List<Item> rewardItem =
            [
                new Item
                {
                    Id = _hashUtil.Generate(),
                    Template = randomItemTplFromPool,
                    ParentId = cultistCircleStashId,
                    SlotId = CircleOfCultistService.CircleOfCultistSlotId,
                    Upd = new Upd
                    {
                        StackObjectsCount = stackSize,
                        SpawnedInSession = true,
                    },
                },
            ];

            // Edge case - item is ammo container and needs cartridges added
            if (_itemHelper.IsOfBaseclass(randomItemTplFromPool, BaseClasses.AMMO_BOX))
            {
                var itemDetails = _itemHelper.GetItem(randomItemTplFromPool).Value;
                _itemHelper.AddCartridgesToAmmoBox(rewardItem, itemDetails);
            }

            // Increment price of rewards to give to player + add to reward array
            rewardItemCount++;
            var singleItemPrice = _itemHelper.GetItemPrice(randomItemTplFromPool);
            var itemPrice = singleItemPrice * stackSize;
            totalRewardCost += (int)itemPrice;

            rewards.Add(rewardItem);
        }

        return rewards;
    }

    /// <summary>
    /// Get direct rewards
    /// </summary>
    /// <param name="sessionId">sessionId</param>
    /// <param name="directReward">Items sacrificed</param>
    /// <param name="cultistCircleStashId">Id of stash item</param>
    /// <returns>The reward object</returns>
    protected List<List<Item>> GetDirectRewards(
        string sessionId,
        DirectRewardSettings directReward,
        string cultistCircleStashId
    )
    {
        // Prep rewards array (reward can be item with children, hence array of arrays)
        List<List<Item>> rewards = [];

        // Handle special case of tagilla helmets - only one reward is allowed
        if (directReward.Reward.Contains(ItemTpl.FACECOVER_TAGILLAS_WELDING_MASK_GORILLA))
        {
            directReward.Reward = [_randomUtil.GetArrayValue(directReward.Reward)];
        }

        // Loop because these can include multiple rewards
        foreach (var rewardTpl in directReward.Reward)
        {
            // Is weapon/armor, handle differently
            if (
                _itemHelper.ArmorItemHasRemovableOrSoftInsertSlots(rewardTpl) ||
                _itemHelper.IsOfBaseclass(rewardTpl, BaseClasses.WEAPON)
            )
            {
                var defaultPreset = _presetHelper.GetDefaultPreset(rewardTpl);
                if (defaultPreset is null)
                {
                    _logger.Warning($"Reward tpl: {rewardTpl} lacks a default preset, skipping reward");

                    continue;
                }

                // Ensure preset has unique ids and is cloned so we don't alter the preset data stored in memory
                var presetAndMods = _itemHelper.ReplaceIDs(defaultPreset.Items);
                _itemHelper.RemapRootItemId(presetAndMods);

                rewards.Add(presetAndMods);

                continue;
            }

            // 'Normal' item, non-preset
            var stackSize = GetDirectRewardBaseTypeStackSize(rewardTpl);
            List<Item> rewardItem =
            [
                new Item
                {
                    Id = _hashUtil.Generate(),
                    Template = rewardTpl,
                    ParentId = cultistCircleStashId,
                    SlotId = CircleOfCultistService.CircleOfCultistSlotId,
                    Upd = new Upd
                    {
                        StackObjectsCount = stackSize,
                        SpawnedInSession = true,
                    },
                },
            ];

            // Edge case - item is ammo container and needs cartridges added
            if (_itemHelper.IsOfBaseclass(rewardTpl, BaseClasses.AMMO_BOX))
            {
                var itemDetails = _itemHelper.GetItem(rewardTpl).Value;
                _itemHelper.AddCartridgesToAmmoBox(rewardItem, itemDetails);
            }

            rewards.Add(rewardItem);
        }

        // Direct reward is not repeatable, flag collected in profile
        if (!directReward.Repeatable)
        {
            FlagDirectRewardAsAcceptedInProfile(sessionId, directReward);
        }

        return rewards;
    }

    /// <summary>
    /// Check for direct rewards from what player sacrificed
    /// </summary>
    /// <param name="sessionId">sessionId</param>
    /// <param name="sacrificedItems">Items sacrificed</param>
    /// <returns>Direct reward items to send to player</returns>
    protected DirectRewardSettings CheckForDirectReward(
        string sessionId,
        List<Item> sacrificedItems,
        Dictionary<string, DirectRewardSettings> directRewardsCache
    )
    {
        // Get sacrificed tpls
        var sacrificedItemTpls = sacrificedItems.Select((item) => item.Template).ToList();
        sacrificedItemTpls.Sort();
        // Create md5 key of the items player sacrificed so we can compare against the direct reward cache
        var sacrificedItemsKey = _hashUtil.GenerateMd5ForData(string.Concat(sacrificedItemTpls, ","));

        var matchingDirectReward = directRewardsCache.GetValueOrDefault(sacrificedItemsKey);
        if (matchingDirectReward is null)
        {
            // No direct reward
            return null;
        }

        var fullProfile = _profileHelper.GetFullProfile(sessionId);
        var directRewardHash = GetDirectRewardHashKey(matchingDirectReward);
        if (fullProfile.SptData.CultistRewards?.ContainsKey(directRewardHash) ?? false)
        {
            // Player has already received this direct reward
            return null;
        }

        return matchingDirectReward;
    }

    /// <summary>
    /// Create an md5 key of the sacrificed + reward items
    /// </summary>
    /// <param name="directReward">Direct reward to create key for</param>
    /// <returns>Key</returns>
    protected string GetDirectRewardHashKey(DirectRewardSettings directReward)
    {
        directReward.RequiredItems.Sort();
        directReward.Reward.Sort();

        var required = string.Concat(directReward.RequiredItems, ",");
        var reward = string.Concat(directReward.Reward, ",");
        // Key is sacrificed items separated by commas, a dash, then the rewards separated by commas
        var key = $"{{{required}-{reward}}}";

        return _hashUtil.GenerateMd5ForData(key);
    }

    /// <summary>
    /// Explicit rewards have their own stack sizes as they don't use a reward rouble pool
    /// </summary>
    /// <param name="rewardTpl">Item being rewarded to get stack size of</param>
    /// <returns>stack size of item</returns>
    protected int GetDirectRewardBaseTypeStackSize(string rewardTpl)
    {
        var itemDetails = _itemHelper.GetItem(rewardTpl);
        if (!itemDetails.Key) {
            _logger.Warning($"{rewardTpl} is not an item, setting stack size to 1");

            return 1;
        }

        // Look for parent in dict
        var settings = _hideoutConfig.CultistCircle.DirectRewardStackSize[itemDetails.Value.Parent];
        if (settings is null) {
            return 1;
        }

        return _randomUtil.GetInt((int)settings.Min, (int)settings.Max);
    }

    /// <summary>
    /// Add a record to the player's profile to signal they have accepted a non-repeatable direct reward
    /// </summary>
    /// <param name="sessionId">Session id</param>
    /// <param name="directReward">Reward sent to player</param>
    protected void FlagDirectRewardAsAcceptedInProfile(string sessionId, DirectRewardSettings directReward)
    {
        var fullProfile = _profileHelper.GetFullProfile(sessionId);
        AcceptedCultistReward dataToStoreInProfile = new AcceptedCultistReward {
            Timestamp = _timeUtil.GetTimeStamp(),
            SacrificeItems = directReward.RequiredItems,
            RewardItems = directReward.Reward,
        };

        fullProfile.SptData.CultistRewards[GetDirectRewardHashKey(directReward)] = dataToStoreInProfile;
    }

    /// <summary>
    /// Get the size of a reward item's stack
    /// 1 for everything except ammo, ammo can be between min stack and max stack
    /// </summary>
    /// <param name="itemTpl">Item chosen</param>
    /// <param name="rewardPoolRemaining">Rouble amount of pool remaining to fill</param>
    /// <returns>Size of stack</returns>
    protected int GetRewardStackSize(string itemTpl, int rewardPoolRemaining)
    {
        if (_itemHelper.IsOfBaseclass(itemTpl, BaseClasses.AMMO)) {
            var ammoTemplate = _itemHelper.GetItem(itemTpl).Value;
            return _itemHelper.GetRandomisedAmmoStackSize(ammoTemplate);
        }

        if (_itemHelper.IsOfBaseclass(itemTpl, BaseClasses.MONEY)) {
            // Get currency-specific values from config
            var settings = _hideoutConfig.CultistCircle.CurrencyRewards[itemTpl];

            // What % of the pool remaining should be rewarded as chosen currency
            var percentOfPoolToUse = _randomUtil.GetInt((int)settings.Min, (int)settings.Max);

            // Rouble amount of pool we want to reward as currency
            var roubleAmountToFill = _randomUtil.GetPercentOfValue(percentOfPoolToUse, rewardPoolRemaining);

            // Convert currency to roubles
            var currencyPriceAsRouble = _itemHelper.GetItemPrice(itemTpl);

            // How many items can we fit into chosen pool
            var itemCountToReward = Math.Round((roubleAmountToFill / currencyPriceAsRouble) ?? 0);

            return (int)itemCountToReward;
        }

        return 1;
    }

    /// <summary>
    /// Get a pool of tpl IDs of items the player needs to complete hideout crafts/upgrade areas
    /// </summary>
    /// <param name="sessionId">Session id</param>
    /// <param name="pmcData">Profile of player who will be getting the rewards</param>
    /// <param name="rewardType">Do we return bonus items (hideout/task items)</param>
    /// <param name="cultistCircleConfig">Circle config</param>
    /// <returns>Array of tpls</returns>
    protected List<string> GetCultistCircleRewardPool(
        string sessionId,
        PmcData pmcData,
        CircleCraftDetails craftingInfo,
        CultistCircleSettings cultistCircleConfig)
    {
        var rewardPool = new HashSet<string>();
        var hideoutDbData = _databaseService.GetHideout();
        var itemsDb = _databaseService.GetItems();

        // Get all items that match the blacklisted types and fold into item blacklist below
        var itemTypeBlacklist = _itemFilterService.GetItemRewardBaseTypeBlacklist();
        var itemsMatchingTypeBlacklist = itemsDb
            .Where((templateItem) => _itemHelper.IsOfBaseclasses(templateItem.Key, itemTypeBlacklist))
            .Select((templateItem) => templateItem.Key);

        // Create set of unique values to ignore
        var itemRewardBlacklist = new HashSet<string>();
        itemRewardBlacklist.UnionWith(_seasonalEventService.GetInactiveSeasonalEventItems());
        itemRewardBlacklist.UnionWith(_itemFilterService.GetItemRewardBlacklist());
        itemRewardBlacklist.UnionWith(cultistCircleConfig.RewardItemBlacklist);
        itemRewardBlacklist.UnionWith(itemsMatchingTypeBlacklist);

        // Hideout and task rewards are ONLY if the bonus is active
        switch (craftingInfo.RewardType) {
            case CircleRewardType.RANDOM: {
                // Does reward pass the high value threshold
                var isHighValueReward = craftingInfo.RewardAmountRoubles >= cultistCircleConfig.HighValueThresholdRub;
                GenerateRandomisedItemsAndAddToRewardPool(rewardPool, itemRewardBlacklist, isHighValueReward);

                break;
            }
            case CircleRewardType.HIDEOUT_TASK: {
                // Hideout/Task loot
                AddHideoutUpgradeRequirementsToRewardPool(hideoutDbData, pmcData, itemRewardBlacklist, rewardPool);
                AddTaskItemRequirementsToRewardPool(pmcData, itemRewardBlacklist, rewardPool);

                // If we have no tasks or hideout stuff left or need more loot to fill it out, default to high value
                if (rewardPool.Count < cultistCircleConfig.MaxRewardItemCount + 2) {
                    GenerateRandomisedItemsAndAddToRewardPool(rewardPool, itemRewardBlacklist, true);
                }
                break;
            }
        }

        // Add custom rewards from config
        if (cultistCircleConfig.AdditionalRewardItemPool.Count > 0) {
            foreach (var additionalReward in cultistCircleConfig.AdditionalRewardItemPool) {
                if (itemRewardBlacklist.Contains(additionalReward)) {
                    continue;
                }

                // Add tpl to reward pool
                rewardPool.Add(additionalReward);
            }
        }

        return rewardPool.ToList();
    }

    /// <summary>
    /// Check player's profile for quests with hand-in requirements and add those required items to the pool
    /// </summary>
    /// <param name="pmcData">Player profile</param>
    /// <param name="itemRewardBlacklist">Items not to add to pool</param>
    /// <param name="rewardPool">Pool to add items to</param>
    protected void AddTaskItemRequirementsToRewardPool(
        PmcData pmcData,
        HashSet<string> itemRewardBlacklist,
        HashSet<string> rewardPool)
    {
        var activeTasks = pmcData.Quests.Where((quest) => quest.Status == QuestStatusEnum.Started);
        foreach (var task in activeTasks) {
            var questData = _questHelper.GetQuestFromDb(task.QId, pmcData);
            var handoverConditions = questData.Conditions.AvailableForFinish.Where(
                (condition) => condition.ConditionType == "HandoverItem"
            );
            foreach (var condition in handoverConditions) {
                foreach (var neededItem in condition.Target.List) {
                    if (itemRewardBlacklist.Contains(neededItem) || !_itemHelper.IsValidItem(neededItem)) {
                        continue;
                    }

                    if (_logger.IsLogEnabled(LogLevel.Debug))
                    {
                        _logger.Debug($"Added Task Loot: {_itemHelper.GetItemName(neededItem)}");
                    }
                    rewardPool.Add(neededItem);
                }
            }
        }
    }

    /// <summary>
    /// Adds items the player needs to complete hideout crafts/upgrades to the reward pool
    /// </summary>
    /// <param name="hideoutDbData">Hideout area data</param>
    /// <param name="pmcData">Player profile</param>
    /// <param name="itemRewardBlacklist">Items not to add to pool</param>
    /// <param name="rewardPool">Pool to add items to</param>
    protected void AddHideoutUpgradeRequirementsToRewardPool(
        Core.Models.Spt.Hideout.Hideout hideoutDbData,
        PmcData pmcData,
        HashSet<string> itemRewardBlacklist,
        HashSet<string> rewardPool)
    {
        var dbAreas = hideoutDbData.Areas;
        foreach (var profileArea in GetPlayerAccessibleHideoutAreas(pmcData.Hideout.Areas)) {
            var currentStageLevel = profileArea.Level;
            var areaType = profileArea.Type;

            // Get next stage of area
            var dbArea = dbAreas.FirstOrDefault((area) => area.Type == areaType);
            var nextStageDbData = dbArea?.Stages[(currentStageLevel + 1).ToString()];
            if (nextStageDbData is not null) {
                // Next stage exists, gather up requirements and add to pool
                var itemRequirements = GetItemRequirements(nextStageDbData.Requirements);
                foreach (var rewardToAdd in itemRequirements) {
                    if (
                        itemRewardBlacklist.Contains(rewardToAdd.TemplateId) ||
                        !_itemHelper.IsValidItem(rewardToAdd.TemplateId)
                    ) {
                        // Dont reward items sacrificed
                        continue;
                    }

                    if (_logger.IsLogEnabled(LogLevel.Debug))
                    {
                        _logger.Debug($"Added Hideout Loot: {_itemHelper.GetItemName(rewardToAdd.TemplateId)}");
                    }
                    rewardPool.Add(rewardToAdd.TemplateId);
                }
            }
        }
    }

    /// <summary>
    /// Get all active hideout areas
    /// </summary>
    /// <param name="areas">Hideout areas to iterate over</param>
    /// <returns>Active area array</returns>
    protected List<BotHideoutArea> GetPlayerAccessibleHideoutAreas(List<BotHideoutArea> areas)
    {
        return areas.Where((area) => {
            if (area.Type == HideoutAreas.CHRISTMAS_TREE && !_seasonalEventService.ChristmasEventEnabled()) {
                // Christmas tree area and not Christmas, skip
                return false;
            }

            return true;
        }).ToList();
    }

    /// <summary>
    /// Get array of random reward items
    /// </summary>
    /// <param name="rewardPool">Reward pool to add to</param>
    /// <param name="itemRewardBlacklist">Item tpls to ignore</param>
    /// <param name="itemsShouldBeHighValue">Should these items meet the valuable threshold</param>
    /// <returns>Set of item tpls</returns>
    protected HashSet<string> GenerateRandomisedItemsAndAddToRewardPool(
        HashSet<string> rewardPool,
        HashSet<string> itemRewardBlacklist,
        bool itemsShouldBeHighValue)
    {
        var allItems = _itemHelper.GetItems();
        var currentItemCount = 0;
        var attempts = 0;
        // `currentItemCount` var will look for the correct number of items, `attempts` var will keep this from never stopping if the highValueThreshold is too high
        while (
            currentItemCount < _hideoutConfig.CultistCircle.MaxRewardItemCount + 2 &&
            attempts < allItems.Count
        ) {
            attempts++;
            var randomItem = _randomUtil.GetArrayValue(allItems);
            if (itemRewardBlacklist.Contains(randomItem.Id) || !_itemHelper.IsValidItem(randomItem.Id)) {
                continue;
            }

            // Valuable check
            if (itemsShouldBeHighValue) {
                var itemValue = _itemHelper.GetItemMaxPrice(randomItem.Id);
                if (itemValue < _hideoutConfig.CultistCircle.HighValueThresholdRub) {
                    continue;
                }
            }

            if (_logger.IsLogEnabled(LogLevel.Debug))
            {
                _logger.Debug($"Added: {_itemHelper.GetItemName(randomItem.Id)}");
            }
            rewardPool.Add(randomItem.Id);
            currentItemCount++;
        }

        return rewardPool;
    }

    /// <summary>
    /// Iterate over passed in hideout requirements and return the Item
    /// </summary>
    /// <param name="requirements">Requirements to iterate over</param>
    /// <returns>Array of item requirements</returns>
    protected List<StageRequirement> GetItemRequirements(List<StageRequirement> requirements)
    {
        return requirements.Where((requirement) => requirement.Type == "Item").ToList();
    }
    
    /// <summary>
    /// Iterate over passed in hideout requirements and return the Item
    /// </summary>
    /// <param name="requirements">Requirements to iterate over</param>
    /// <returns>Array of item requirements</returns>
    protected  List<Requirement> GetItemRequirements(List<Requirement> requirements)
    {
        return requirements.Where((requirement) => requirement.Type == "Item").ToList();
    }

    /// <summary>
    /// Create a map of the possible direct rewards, keyed by the items needed to be sacrificed
    /// </summary>
    /// <param name="directRewards">Direct rewards array from hideout config</param>
    /// <returns>Dictionary</returns>
    protected Dictionary<string, DirectRewardSettings> GenerateSacrificedItemsCache(List<DirectRewardSettings> directRewards)
    {
        var result = new Dictionary<string, DirectRewardSettings>();
        foreach (var rewardSettings in directRewards) {
            rewardSettings.RequiredItems.Sort();
            var concat = string.Concat(rewardSettings.RequiredItems, ",");
            
            var key = _hashUtil.GenerateMd5ForData(concat);
            result[key] = rewardSettings;
        }

        return result;
    }

    /// <summary>
    /// Attempt to add all rewards to cultist circle, if they don't fit remove one and try again until they fit
    /// </summary>
    /// <param name="sessionId">Session id</param>
    /// <param name="pmcData">Player profile</param>
    /// <param name="rewards">Rewards to send to player</param>
    /// <param name="containerGrid">Cultist grid to add rewards to</param>
    /// <param name="cultistCircleStashId">Stash id</param>
    /// <param name="output">Client output</param>
    protected void AddRewardsToCircleContainer(
        string sessionId,
        PmcData pmcData,
        List<List<Item>> rewards,
        int[][] containerGrid,
        string cultistCircleStashId,
        ItemEventRouterResponse output
    )
    {
        var canAddToContainer = false;
        while (!canAddToContainer && rewards.Count > 0) {
            canAddToContainer = _inventoryHelper.CanPlaceItemsInContainer(
                _cloner.Clone(containerGrid), // MUST clone grid before passing in as function modifies grid
                rewards
            );

            // Doesn't fit, remove one item
            if (!canAddToContainer) {
                rewards.PopFirst();
            }
        }

        foreach (var itemToAdd in rewards) {
            _inventoryHelper.PlaceItemInContainer(
                containerGrid,
                itemToAdd,
                cultistCircleStashId,
                CircleOfCultistService.CircleOfCultistSlotId
            );
            // Add item + mods to output and profile inventory
            output.ProfileChanges[sessionId].Items.NewItems.AddRange(itemToAdd);
            pmcData.Inventory.Items.AddRange(itemToAdd);
        }
    }
}
