import { inject, injectable } from "tsyringe";

import { BotWeaponGenerator } from "@spt-aki/generators/BotWeaponGenerator";
import { BotGeneratorHelper } from "@spt-aki/helpers/BotGeneratorHelper";
import { BotHelper } from "@spt-aki/helpers/BotHelper";
import { HandbookHelper } from "@spt-aki/helpers/HandbookHelper";
import { InventoryHelper } from "@spt-aki/helpers/InventoryHelper";
import { ItemHelper } from "@spt-aki/helpers/ItemHelper";
import { WeightedRandomHelper } from "@spt-aki/helpers/WeightedRandomHelper";
import { Inventory as PmcInventory } from "@spt-aki/models/eft/common/tables/IBotBase";
import { IBotType, Inventory, ModsChances } from "@spt-aki/models/eft/common/tables/IBotType";
import { Item } from "@spt-aki/models/eft/common/tables/IItem";
import { ITemplateItem } from "@spt-aki/models/eft/common/tables/ITemplateItem";
import { BaseClasses } from "@spt-aki/models/enums/BaseClasses";
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
import { EquipmentSlots } from "@spt-aki/models/enums/EquipmentSlots";
import { ItemAddedResult } from "@spt-aki/models/enums/ItemAddedResult";
import { LootCacheType } from "@spt-aki/models/spt/bots/IBotLootCache";
import { IItemSpawnLimitSettings } from "@spt-aki/models/spt/bots/IItemSpawnLimitSettings";
import { IBotConfig } from "@spt-aki/models/spt/config/IBotConfig";
import { IPmcConfig } from "@spt-aki/models/spt/config/IPmcConfig";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { ConfigServer } from "@spt-aki/servers/ConfigServer";
import { DatabaseServer } from "@spt-aki/servers/DatabaseServer";
import { BotLootCacheService } from "@spt-aki/services/BotLootCacheService";
import { LocalisationService } from "@spt-aki/services/LocalisationService";
import { HashUtil } from "@spt-aki/utils/HashUtil";
import { JsonUtil } from "@spt-aki/utils/JsonUtil";
import { RandomUtil } from "@spt-aki/utils/RandomUtil";

@injectable()
export class BotLootGenerator
{
    protected botConfig: IBotConfig;
    protected pmcConfig: IPmcConfig;

    constructor(
        @inject("WinstonLogger") protected logger: ILogger,
        @inject("HashUtil") protected hashUtil: HashUtil,
        @inject("RandomUtil") protected randomUtil: RandomUtil,
        @inject("ItemHelper") protected itemHelper: ItemHelper,
        @inject("JsonUtil") protected jsonUtil: JsonUtil,
        @inject("InventoryHelper") protected inventoryHelper: InventoryHelper,
        @inject("DatabaseServer") protected databaseServer: DatabaseServer,
        @inject("HandbookHelper") protected handbookHelper: HandbookHelper,
        @inject("BotGeneratorHelper") protected botGeneratorHelper: BotGeneratorHelper,
        @inject("BotWeaponGenerator") protected botWeaponGenerator: BotWeaponGenerator,
        @inject("WeightedRandomHelper") protected weightedRandomHelper: WeightedRandomHelper,
        @inject("BotHelper") protected botHelper: BotHelper,
        @inject("BotLootCacheService") protected botLootCacheService: BotLootCacheService,
        @inject("LocalisationService") protected localisationService: LocalisationService,
        @inject("ConfigServer") protected configServer: ConfigServer,
    )
    {
        this.botConfig = this.configServer.getConfig(ConfigTypes.BOT);
        this.pmcConfig = this.configServer.getConfig(ConfigTypes.PMC);
    }

    protected getItemSpawnLimitsForBot(botRole: string): IItemSpawnLimitSettings
    {
        // Init item limits
        const limitsForBotDict: Record<string, number> = {};
        this.initItemLimitArray(botRole, limitsForBotDict);

        return { currentLimits: limitsForBotDict, globalLimits: this.getItemSpawnLimitsForBotType(botRole) };
    }

    /**
     * Add loot to bots containers
     * @param sessionId Session id
     * @param botJsonTemplate Base json db file for the bot having its loot generated
     * @param isPmc Will bot be a pmc
     * @param botRole Role of bot, e.g. asssult
     * @param botInventory Inventory to add loot to
     * @param botLevel Level of bot
     */
    public generateLoot(
        sessionId: string,
        botJsonTemplate: IBotType,
        isPmc: boolean,
        botRole: string,
        botInventory: PmcInventory,
        botLevel: number,
    ): void
    {
        // Limits on item types to be added as loot
        const itemCounts = botJsonTemplate.generation.items;

        const backpackLootCount = Number(
            this.weightedRandomHelper.getWeightedValue<number>(itemCounts.backpackLoot.weights),
        );
        const pocketLootCount = Number(
            this.weightedRandomHelper.getWeightedValue<number>(itemCounts.pocketLoot.weights),
        );
        const vestLootCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.vestLoot.weights);
        const specialLootItemCount = Number(
            this.weightedRandomHelper.getWeightedValue<number>(itemCounts.specialItems.weights),
        );
        const healingItemCount = Number(this.weightedRandomHelper.getWeightedValue<number>(itemCounts.healing.weights));
        const drugItemCount = Number(this.weightedRandomHelper.getWeightedValue<number>(itemCounts.drugs.weights));

        const foodItemCount = Number(this.weightedRandomHelper.getWeightedValue<number>(itemCounts.food.weights));
        const drinkItemCount = Number(this.weightedRandomHelper.getWeightedValue<number>(itemCounts.drink.weights));

        const currencyItemCount = Number(
            this.weightedRandomHelper.getWeightedValue<number>(itemCounts.currency.weights),
        );

        const stimItemCount = Number(this.weightedRandomHelper.getWeightedValue<number>(itemCounts.stims.weights));
        const grenadeCount = Number(this.weightedRandomHelper.getWeightedValue<number>(itemCounts.grenades.weights));

        // Forced pmc healing loot
        if (isPmc && this.pmcConfig.forceHealingItemsIntoSecure)
        {
            this.addForcedMedicalItemsToPmcSecure(botInventory, botRole);
        }

        const botItemLimits = this.getItemSpawnLimitsForBot(botRole);

        const containersBotHasAvailable = this.getAvailableContainersBotCanStoreItemsIn(botInventory);

        // This set is passed as a reference to fill up the containers that are already full, this aliviates
        // generation of the bots by avoiding checking the slots of containers we already know are full
        const containersIdFull = new Set<string>();

        // Special items
        this.addLootFromPool(
            this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.SPECIAL, botJsonTemplate),
            containersBotHasAvailable,
            specialLootItemCount,
            botInventory,
            botRole,
            botItemLimits,
            undefined,
            undefined,
            containersIdFull,
        );

        // Healing items / Meds
        this.addLootFromPool(
            this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.HEALING_ITEMS, botJsonTemplate),
            containersBotHasAvailable,
            healingItemCount,
            botInventory,
            botRole,
            null,
            0,
            isPmc,
            containersIdFull,
        );

        // Drugs
        this.addLootFromPool(
            this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.DRUG_ITEMS, botJsonTemplate),
            containersBotHasAvailable,
            drugItemCount,
            botInventory,
            botRole,
            null,
            0,
            isPmc,
            containersIdFull,
        );

        // Food
        this.addLootFromPool(
            this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.FOOD_ITEMS, botJsonTemplate),
            containersBotHasAvailable,
            foodItemCount,
            botInventory,
            botRole,
            null,
            0,
            isPmc,
            containersIdFull,
        );

        // Drink
        this.addLootFromPool(
            this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.DRINK_ITEMS, botJsonTemplate),
            containersBotHasAvailable,
            drinkItemCount,
            botInventory,
            botRole,
            null,
            0,
            isPmc,
            containersIdFull,
        );

        // Currency
        this.addLootFromPool(
            this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.CURRENCY_ITEMS, botJsonTemplate),
            containersBotHasAvailable,
            currencyItemCount,
            botInventory,
            botRole,
            null,
            0,
            isPmc,
            containersIdFull,
        );

        // Stims
        this.addLootFromPool(
            this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.STIM_ITEMS, botJsonTemplate),
            containersBotHasAvailable,
            stimItemCount,
            botInventory,
            botRole,
            botItemLimits,
            0,
            isPmc,
            containersIdFull,
        );

        // Grenades
        this.addLootFromPool(
            this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.GRENADE_ITEMS, botJsonTemplate),
            [EquipmentSlots.POCKETS, EquipmentSlots.TACTICAL_VEST], // Can't use containersBotHasEquipped as we dont want grenades added to backpack
            grenadeCount,
            botInventory,
            botRole,
            null,
            0,
            isPmc,
            containersIdFull,
        );

        // Backpack - generate loot if they have one
        if (containersBotHasAvailable.includes(EquipmentSlots.BACKPACK))
        {
            // Add randomly generated weapon to PMC backpacks
            if (isPmc && this.randomUtil.getChance100(this.pmcConfig.looseWeaponInBackpackChancePercent))
            {
                this.addLooseWeaponsToInventorySlot(
                    sessionId,
                    botInventory,
                    EquipmentSlots.BACKPACK,
                    botJsonTemplate.inventory,
                    botJsonTemplate.chances.weaponMods,
                    botRole,
                    isPmc,
                    botLevel,
                    containersIdFull,
                );
            }

            this.addLootFromPool(
                this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.BACKPACK, botJsonTemplate),
                [EquipmentSlots.BACKPACK],
                backpackLootCount,
                botInventory,
                botRole,
                botItemLimits,
                this.pmcConfig.maxBackpackLootTotalRub,
                isPmc,
                containersIdFull,
            );
        }

        // TacticalVest - generate loot if they have one
        if (containersBotHasAvailable.includes(EquipmentSlots.TACTICAL_VEST))
        {
            // Vest
            this.addLootFromPool(
                this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.VEST, botJsonTemplate),
                [EquipmentSlots.TACTICAL_VEST],
                vestLootCount,
                botInventory,
                botRole,
                botItemLimits,
                this.pmcConfig.maxVestLootTotalRub,
                isPmc,
                containersIdFull,
            );
        }

        // Pockets
        this.addLootFromPool(
            this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.POCKET, botJsonTemplate),
            [EquipmentSlots.POCKETS],
            pocketLootCount,
            botInventory,
            botRole,
            botItemLimits,
            this.pmcConfig.maxPocketLootTotalRub,
            isPmc,
            containersIdFull,
        );

        // Secure
        this.addLootFromPool(
            this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.SECURE, botJsonTemplate),
            [EquipmentSlots.SECURED_CONTAINER],
            50,
            botInventory,
            botRole,
            null,
            -1,
            isPmc,
            containersIdFull,
        );
    }

    /**
     * Get an array of the containers a bot has on them (pockets/backpack/vest)
     * @param botInventory Bot to check
     * @returns Array of available slots
     */
    protected getAvailableContainersBotCanStoreItemsIn(botInventory: PmcInventory): EquipmentSlots[]
    {
        const result = [EquipmentSlots.POCKETS];

        if (botInventory.items.find((item) => item.slotId === EquipmentSlots.TACTICAL_VEST))
        {
            result.push(EquipmentSlots.TACTICAL_VEST);
        }

        if (botInventory.items.find((item) => item.slotId === EquipmentSlots.BACKPACK))
        {
            result.push(EquipmentSlots.BACKPACK);
        }

        return result;
    }

    /**
     * Force healing items onto bot to ensure they can heal in-raid
     * @param botInventory Inventory to add items to
     * @param botRole Role of bot (sptBear/sptUsec)
     */
    protected addForcedMedicalItemsToPmcSecure(botInventory: PmcInventory, botRole: string): void
    {
        // surv12
        this.addLootFromPool(
            // eslint-disable-next-line @typescript-eslint/naming-convention
            { "5d02797c86f774203f38e30a": 1 },
            [EquipmentSlots.SECURED_CONTAINER],
            1,
            botInventory,
            botRole,
            null,
            0,
            true,
        );

        // AFAK
        this.addLootFromPool(
            // eslint-disable-next-line @typescript-eslint/naming-convention
            { "60098ad7c2240c0fe85c570a": 1 },
            [EquipmentSlots.SECURED_CONTAINER],
            10,
            botInventory,
            botRole,
            null,
            0,
            true,
        );
    }

    /**
     * Get a biased random number
     * @param min Smallest size
     * @param max Biggest size
     * @param nValue Value to bias choice
     * @returns Chosen number
     */
    protected getRandomisedCount(min: number, max: number, nValue: number): number
    {
        const range = max - min;
        return this.randomUtil.getBiasedRandomNumber(min, max, range, nValue);
    }

    /**
     * Take random items from a pool and add to an inventory until totalItemCount or totalValueLimit or space limit is reached
     * @param pool Pool of items to pick from with weight
     * @param equipmentSlots What equipment slot will the loot items be added to
     * @param totalItemCount Max count of items to add
     * @param inventoryToAddItemsTo Bot inventory loot will be added to
     * @param botRole Role of the bot loot is being generated for (assault/pmcbot)
     * @param itemSpawnLimits Item spawn limits the bot must adhere to
     * @param totalValueLimitRub Total value of loot allowed in roubles
     * @param isPmc Is bot being generated for a pmc
     */
    protected addLootFromPool(
        pool: Record<string, number>,
        equipmentSlots: string[],
        totalItemCount: number,
        inventoryToAddItemsTo: PmcInventory,
        botRole: string,
        itemSpawnLimits: IItemSpawnLimitSettings = null,
        totalValueLimitRub = 0,
        isPmc = false,
        containersIdFull = new Set<string>(),
    ): void
    {
        // Loot pool has items
        const poolSize = Object.keys(pool).length;
        if (poolSize > 0)
        {
            let currentTotalRub = 0;

            let fitItemIntoContainerAttempts = 0;
            for (let i = 0; i < totalItemCount; i++)
            {
                // Pool can become empty if item spawn limits keep removing items
                if (Object.keys(pool).length === 0)
                {
                    return;
                }

                const weightedItemTpl = this.weightedRandomHelper.getWeightedValue<string>(pool);
                const itemResult = this.itemHelper.getItem(weightedItemTpl);
                const itemToAddTemplate = itemResult[1];
                if (!itemResult[0])
                {
                    this.logger.warning(
                        `Unable to process item tpl: ${weightedItemTpl} for slots: ${equipmentSlots} on bot: ${botRole}`,
                    );

                    continue;
                }

                if (itemSpawnLimits)
                {
                    if (this.itemHasReachedSpawnLimit(itemToAddTemplate, botRole, itemSpawnLimits))
                    {
                        // Remove item from pool to prevent it being picked again
                        delete pool[weightedItemTpl];

                        i--;
                        continue;
                    }
                }

                const newRootItemId = this.hashUtil.generate();
                const itemWithChildrenToAdd: Item[] = [{
                    _id: newRootItemId,
                    _tpl: itemToAddTemplate._id,
                    ...this.botGeneratorHelper.generateExtraPropertiesForItem(itemToAddTemplate, botRole),
                }];

                // Is Simple-Wallet / WZ wallet
                if (this.botConfig.walletLoot.walletTplPool.includes(weightedItemTpl))
                {
                    const addCurrencyToWallet = this.randomUtil.getChance100(this.botConfig.walletLoot.chancePercent);
                    if (addCurrencyToWallet)
                    {
                        // Create the currency items we want to add to wallet
                        const itemsToAdd = this.createWalletLoot(newRootItemId);

                        // Get the container grid for the wallet
                        const containerGrid = this.inventoryHelper.getContainerSlotMap(weightedItemTpl);

                        // Check if all the chosen currency items fit into wallet
                        const canAddToContainer = this.inventoryHelper.canPlaceItemsInContainer(
                            this.jsonUtil.clone(containerGrid), // MUST clone grid before passing in as function modifies grid
                            itemsToAdd,
                        );
                        if (canAddToContainer)
                        {
                            // Add each currency to wallet
                            for (const itemToAdd of itemsToAdd)
                            {
                                this.inventoryHelper.placeItemInContainer(
                                    containerGrid,
                                    itemToAdd,
                                    itemWithChildrenToAdd[0]._id,
                                    "main",
                                );
                            }

                            itemWithChildrenToAdd.push(...itemsToAdd.flatMap((moneyStack) => moneyStack));
                        }
                    }
                }
                // Some items (ammBox/ammo) need extra changes
                this.addRequiredChildItemsToParent(itemToAddTemplate, itemWithChildrenToAdd, isPmc, botRole);

                // Attempt to add item to container(s)
                const itemAddedResult = this.botGeneratorHelper.addItemWithChildrenToEquipmentSlot(
                    equipmentSlots,
                    newRootItemId,
                    itemToAddTemplate._id,
                    itemWithChildrenToAdd,
                    inventoryToAddItemsTo,
                    containersIdFull,
                );

                // Handle when item cannot be added
                if (itemAddedResult !== ItemAddedResult.SUCCESS)
                {
                    if (itemAddedResult === ItemAddedResult.NO_CONTAINERS)
                    {
                        // Bot has no container to put item in, exit
                        this.logger.debug(
                            `Unable to add: ${totalItemCount} items to bot as it lacks a container to include them`,
                        );
                        break;
                    }

                    fitItemIntoContainerAttempts++;
                    if (fitItemIntoContainerAttempts >= 4)
                    {
                        this.logger.debug(
                            `Failed to place item ${i} of ${totalItemCount} items into ${botRole} containers: ${
                                equipmentSlots.join(",")
                            }. Tried ${fitItemIntoContainerAttempts} times, reason: ${
                                ItemAddedResult[itemAddedResult]
                            }, skipping`,
                        );

                        break;
                    }

                    // Try again, failed but still under attempt limit
                    continue;
                }

                // Item added okay, reset counter for next item
                fitItemIntoContainerAttempts = 0;

                // Stop adding items to bots pool if rolling total is over total limit
                if (totalValueLimitRub > 0)
                {
                    currentTotalRub += this.handbookHelper.getTemplatePrice(itemToAddTemplate._id);
                    if (currentTotalRub > totalValueLimitRub)
                    {
                        break;
                    }
                }
            }
        }
    }

    protected createWalletLoot(walletId: string): Item[][]
    {
        const result: Item[][] = [];

        // Choose how many stacks of currency will be added to wallet
        const itemCount = this.randomUtil.getInt(
            this.botConfig.walletLoot.itemCount.min,
            this.botConfig.walletLoot.itemCount.max,
        );
        for (let index = 0; index < itemCount; index++)
        {
            // Choose the size of the currency stack - default is 5k, 10k, 15k, 20k, 25k
            const chosenStackCount = Number(
                this.weightedRandomHelper.getWeightedValue<string>(this.botConfig.walletLoot.stackSizeWeight),
            );
            result.push([{
                _id: this.hashUtil.generate(),
                _tpl: this.weightedRandomHelper.getWeightedValue<string>(this.botConfig.walletLoot.currencyWeight),
                parentId: walletId,
                upd: { StackObjectsCount: chosenStackCount },
            }]);
        }

        return result;
    }

    /**
     * Some items need child items to function, add them to the itemToAddChildrenTo array
     * @param itemToAddTemplate Db template of item to check
     * @param itemToAddChildrenTo Item to add children to
     * @param isPmc Is the item being generated for a pmc (affects money/ammo stack sizes)
     * @param botRole role bot has that owns item
     */
    protected addRequiredChildItemsToParent(
        itemToAddTemplate: ITemplateItem,
        itemToAddChildrenTo: Item[],
        isPmc: boolean,
        botRole: string,
    ): void
    {
        // Fill ammo box
        if (this.itemHelper.isOfBaseclass(itemToAddTemplate._id, BaseClasses.AMMO_BOX))
        {
            this.itemHelper.addCartridgesToAmmoBox(itemToAddChildrenTo, itemToAddTemplate);
        }
        // Make money a stack
        else if (this.itemHelper.isOfBaseclass(itemToAddTemplate._id, BaseClasses.MONEY))
        {
            this.randomiseMoneyStackSize(botRole, itemToAddTemplate, itemToAddChildrenTo[0]);
        }
        // Make ammo a stack
        else if (this.itemHelper.isOfBaseclass(itemToAddTemplate._id, BaseClasses.AMMO))
        {
            this.randomiseAmmoStackSize(isPmc, itemToAddTemplate, itemToAddChildrenTo[0]);
        }
        // Must add soft inserts/plates
        else if (this.itemHelper.itemRequiresSoftInserts(itemToAddTemplate._id))
        {
            this.itemHelper.addChildSlotItems(itemToAddChildrenTo, itemToAddTemplate, null, false);
        }
    }

    /**
     * Add generated weapons to inventory as loot
     * @param botInventory inventory to add preset to
     * @param equipmentSlot slot to place the preset in (backpack)
     * @param templateInventory bots template, assault.json
     * @param modChances chances for mods to spawn on weapon
     * @param botRole bots role .e.g. pmcBot
     * @param isPmc are we generating for a pmc
     */
    protected addLooseWeaponsToInventorySlot(
        sessionId: string,
        botInventory: PmcInventory,
        equipmentSlot: string,
        templateInventory: Inventory,
        modChances: ModsChances,
        botRole: string,
        isPmc: boolean,
        botLevel: number,
        containersIdFull?: Set<string>,
    ): void
    {
        const chosenWeaponType = this.randomUtil.getArrayValue([
            EquipmentSlots.FIRST_PRIMARY_WEAPON,
            EquipmentSlots.FIRST_PRIMARY_WEAPON,
            EquipmentSlots.FIRST_PRIMARY_WEAPON,
            EquipmentSlots.HOLSTER,
        ]);
        const randomisedWeaponCount = this.randomUtil.getInt(
            this.pmcConfig.looseWeaponInBackpackLootMinMax.min,
            this.pmcConfig.looseWeaponInBackpackLootMinMax.max,
        );
        if (randomisedWeaponCount > 0)
        {
            for (let i = 0; i < randomisedWeaponCount; i++)
            {
                const generatedWeapon = this.botWeaponGenerator.generateRandomWeapon(
                    sessionId,
                    chosenWeaponType,
                    templateInventory,
                    botInventory.equipment,
                    modChances,
                    botRole,
                    isPmc,
                    botLevel,
                );
                const result = this.botGeneratorHelper.addItemWithChildrenToEquipmentSlot(
                    [equipmentSlot],
                    generatedWeapon.weapon[0]._id,
                    generatedWeapon.weapon[0]._tpl,
                    [...generatedWeapon.weapon],
                    botInventory,
                    containersIdFull,
                );

                if (result !== ItemAddedResult.SUCCESS)
                {
                    this.logger.debug(
                        `Failed to add additional weapon ${generatedWeapon.weapon[0]._id} to bot backpack, reason: ${
                            ItemAddedResult[result]
                        }`,
                    );
                }
            }
        }
    }

    /**
     * Hydrate item limit array to contain items that have a limit for a specific bot type
     * All values are set to 0
     * @param botRole Role the bot has
     * @param limitCount
     */
    protected initItemLimitArray(botRole: string, limitCount: Record<string, number>): void
    {
        // Init current count of items we want to limit
        const spawnLimits = this.getItemSpawnLimitsForBotType(botRole);
        for (const limit in spawnLimits)
        {
            limitCount[limit] = 0;
        }
    }

    /**
     * Check if an item has reached its bot-specific spawn limit
     * @param itemTemplate Item we check to see if its reached spawn limit
     * @param botRole Bot type
     * @param itemSpawnLimits
     * @returns true if item has reached spawn limit
     */
    protected itemHasReachedSpawnLimit(
        itemTemplate: ITemplateItem,
        botRole: string,
        itemSpawnLimits: IItemSpawnLimitSettings,
    ): boolean
    {
        // PMCs and scavs have different sections of bot config for spawn limits
        if (!!itemSpawnLimits && Object.keys(itemSpawnLimits.globalLimits).length === 0)
        {
            // No items found in spawn limit, drop out
            return false;
        }

        // No spawn limits, skipping
        if (!itemSpawnLimits)
        {
            return false;
        }

        const idToCheckFor = this.getMatchingIdFromSpawnLimits(itemTemplate, itemSpawnLimits.globalLimits);
        if (!idToCheckFor)
        {
            // ParentId or tplid not found in spawnLimits, not a spawn limited item, skip
            return false;
        }

        // Increment item count with this bot type
        itemSpawnLimits.currentLimits[idToCheckFor]++;

        // Check if over limit
        if (itemSpawnLimits.currentLimits[idToCheckFor] > itemSpawnLimits.globalLimits[idToCheckFor])
        {
            // Prevent edge-case of small loot pools + code trying to add limited item over and over infinitely
            if (itemSpawnLimits.currentLimits[idToCheckFor] > itemSpawnLimits[idToCheckFor] * 10)
            {
                this.logger.debug(
                    this.localisationService.getText("bot-item_spawn_limit_reached_skipping_item", {
                        botRole: botRole,
                        itemName: itemTemplate._name,
                        attempts: itemSpawnLimits.currentLimits[idToCheckFor],
                    }),
                );

                return false;
            }

            return true;
        }

        return false;
    }

    /**
     * Randomise the stack size of a money object, uses different values for pmc or scavs
     * @param botRole Role bot has that has money stack
     * @param itemTemplate item details from db
     * @param moneyItem Money item to randomise
     */
    protected randomiseMoneyStackSize(botRole: string, itemTemplate: ITemplateItem, moneyItem: Item): void
    {
        // Get all currency weights for this bot type
        let currencyWeights = this.botConfig.currencyStackSize[botRole];
        if (!currencyWeights)
        {
            currencyWeights = this.botConfig.currencyStackSize.default;
        }

        const currencyWeight = currencyWeights[moneyItem._tpl];

        this.itemHelper.addUpdObjectToItem(moneyItem);

        moneyItem.upd.StackObjectsCount = Number.parseInt(this.weightedRandomHelper.getWeightedValue(currencyWeight));
    }

    /**
     * Randomise the size of an ammo stack
     * @param isPmc Is ammo on a PMC bot
     * @param itemTemplate item details from db
     * @param ammoItem Ammo item to randomise
     */
    protected randomiseAmmoStackSize(isPmc: boolean, itemTemplate: ITemplateItem, ammoItem: Item): void
    {
        const randomSize = itemTemplate._props.StackMaxSize === 1
            ? 1
            : this.randomUtil.getInt(
                itemTemplate._props.StackMinRandom,
                Math.min(itemTemplate._props.StackMaxRandom, 60),
            );

        this.itemHelper.addUpdObjectToItem(ammoItem);

        ammoItem.upd.StackObjectsCount = randomSize;
    }

    /**
     * Get spawn limits for a specific bot type from bot.json config
     * If no limit found for a non pmc bot, fall back to defaults
     * @param botRole what role does the bot have
     * @returns Dictionary of tplIds and limit
     */
    protected getItemSpawnLimitsForBotType(botRole: string): Record<string, number>
    {
        if (this.botHelper.isBotPmc(botRole))
        {
            return this.botConfig.itemSpawnLimits.pmc;
        }

        if (this.botConfig.itemSpawnLimits[botRole.toLowerCase()])
        {
            return this.botConfig.itemSpawnLimits[botRole.toLowerCase()];
        }

        this.logger.warning(
            this.localisationService.getText("bot-unable_to_find_spawn_limits_fallback_to_defaults", botRole),
        );

        return this.botConfig.itemSpawnLimits.default;
    }

    /**
     * Get the parentId or tplId of item inside spawnLimits object if it exists
     * @param itemTemplate item we want to look for in spawn limits
     * @param spawnLimits Limits to check for item
     * @returns id as string, otherwise undefined
     */
    protected getMatchingIdFromSpawnLimits(itemTemplate: ITemplateItem, spawnLimits: Record<string, number>): string
    {
        if (itemTemplate._id in spawnLimits)
        {
            return itemTemplate._id;
        }

        // tplId not found in spawnLimits, check if parentId is
        if (itemTemplate._parent in spawnLimits)
        {
            return itemTemplate._parent;
        }

        // parentId and tplid not found
        return undefined;
    }
}