using System.Text.Json.Serialization;
using Core.Helpers;
using Core.Models.Eft.Common;
using Core.Models.Eft.Common.Tables;
using Core.Models.Enums;
using Core.Models.Spt.Config;
using Core.Models.Utils;
using Core.Servers;
using Core.Services;
using Core.Utils;
using Core.Utils.Cloners;
using Core.Utils.Collections;
using SptCommon.Annotations;
using LogLevel = Core.Models.Spt.Logging.LogLevel;

namespace Core.Generators;

[Injectable]
public class LocationLootGenerator(
    ISptLogger<LocationLootGenerator> _logger,
    RandomUtil _randomUtil,
    MathUtil _mathUtil,
    HashUtil _hashUtil,
    ItemHelper _itemHelper,
    InventoryHelper _inventoryHelper,
    DatabaseService _databaseService,
    ContainerHelper _containerHelper,
    PresetHelper _presetHelper,
    LocalisationService _localisationService,
    SeasonalEventService _seasonalEventService,
    ItemFilterService _itemFilterService,
    ConfigServer _configServer,
    ICloner _cloner
)
{
    protected LocationConfig _locationConfig = _configServer.GetConfig<LocationConfig>();
    protected SeasonalEventConfig _seasonalEventConfig = _configServer.GetConfig<SeasonalEventConfig>();

    /// Create a list of container objects with randomised loot
    /// <param name="locationBase">Map base to generate containers for</param>
    /// <param name="staticAmmoDist">Static ammo distribution</param>
    /// <returns>List of container objects</returns>
    public List<SpawnpointTemplate> GenerateStaticContainers(LocationBase locationBase,
        Dictionary<string, List<StaticAmmoDetails>> staticAmmoDist)
    {
        var staticLootItemCount = 0;
        var result = new List<SpawnpointTemplate>();
        var locationId = locationBase.Id.ToLower();

        var mapData = _databaseService.GetLocation(locationId);

        var staticWeaponsOnMapClone = _cloner.Clone(mapData.StaticContainers.Value.StaticWeapons);
        if (staticWeaponsOnMapClone is null)
        {
            _logger.Error(
                _localisationService.GetText("location-unable_to_find_static_weapon_for_map", locationBase.Name)
            );
        }

        // Add mounted weapons to output loot
        result.AddRange(staticWeaponsOnMapClone);

        var allStaticContainersOnMapClone = _cloner.Clone(mapData.StaticContainers.Value.StaticContainers);
        if (allStaticContainersOnMapClone is null)
        {
            _logger.Error(
                _localisationService.GetText("location-unable_to_find_static_container_for_map", locationBase.Name)
            );
        }

        // Containers that MUST be added to map (e.g. quest containers)
        var staticForcedOnMapClone = _cloner.Clone(mapData.StaticContainers.Value.StaticForced);
        if (staticForcedOnMapClone is null)
        {
            _logger.Error(
                _localisationService.GetText(
                    "location-unable_to_find_forced_static_data_for_map",
                    locationBase.Name
                )
            );
        }

        // Remove christmas items from loot data
        if (!_seasonalEventService.ChristmasEventEnabled())
        {
            allStaticContainersOnMapClone = allStaticContainersOnMapClone.Where(
                    item => !_seasonalEventConfig.ChristmasContainerIds.Contains(item.Template.Id)
                )
                .ToList();
        }

        var staticRandomisableContainersOnMap = GetRandomisableContainersOnMap(allStaticContainersOnMapClone);

        // Keep track of static loot count
        var staticContainerCount = 0;

        // Find all 100% spawn containers
        var staticLootDist = mapData.StaticLoot;
        var guaranteedContainers = GetGuaranteedContainers(allStaticContainersOnMapClone);
        staticContainerCount += guaranteedContainers.Count;

        // Add loot to guaranteed containers and add to result
        foreach (var container in guaranteedContainers)
        {
            var containerWithLoot = AddLootToContainer(
                container,
                staticForcedOnMapClone,
                staticLootDist.Value,
                staticAmmoDist,
                locationId
            );
            result.Add(containerWithLoot.Template);

            staticLootItemCount += containerWithLoot.Template.Items.Count;
        }

        if(_logger.IsLogEnabled(LogLevel.Debug))
        {
            _logger.Debug($"Added {guaranteedContainers.Count} guaranteed containers");
        }

        // Randomisation is turned off globally or just turned off for this map
        if (
            !(
                _locationConfig.ContainerRandomisationSettings.Enabled &&
                _locationConfig.ContainerRandomisationSettings.Maps[locationId]
            )
        )
        {
            if(_logger.IsLogEnabled(LogLevel.Debug))
            {
                _logger.Debug(
                    $"Container randomisation disabled, Adding {staticRandomisableContainersOnMap.Count} containers to {locationBase.Name}"
                );
            }
            foreach (var container in staticRandomisableContainersOnMap)
            {
                var containerWithLoot = AddLootToContainer(
                    container,
                    staticForcedOnMapClone,
                    staticLootDist.Value,
                    staticAmmoDist,
                    locationId
                );
                result.Add(containerWithLoot.Template);

                staticLootItemCount += containerWithLoot.Template.Items.Count;
            }

            _logger.Success($"A total of {staticLootItemCount} static items spawned");

            return result;
        }

        // Group containers by their groupId
        if (mapData.Statics is null)
        {
            _logger.Warning(_localisationService.GetText("location-unable_to_generate_static_loot", locationId));

            return result;
        }

        var mapping = GetGroupIdToContainerMappings(mapData.Statics, staticRandomisableContainersOnMap);

        // For each of the container groups, choose from the pool of containers, hydrate container with loot and add to result array
        foreach (var (key, data) in mapping)
        {
            // Count chosen was 0, skip
            if (data.ChosenCount == 0)
            {
                continue;
            }

            if (data.ContainerIdsWithProbability.Count == 0)
            {
                if(_logger.IsLogEnabled(LogLevel.Debug))
                {
                    _logger.Debug($"`Group: {key} has no containers with< 100 % spawn chance to choose from, skipping");
                }

                continue;
            }

            // EDGE CASE: These are containers without a group and have a probability < 100%
            if (key == "")
            {
                var containerIdsCopy = _cloner.Clone(data.ContainerIdsWithProbability);
                // Roll each containers probability, if it passes, it gets added
                data.ContainerIdsWithProbability = new Dictionary<string, double>();
                foreach (var containerId in containerIdsCopy)
                    if (_randomUtil.GetChance100(containerIdsCopy[containerId.Key] * 100))
                    {
                        data.ContainerIdsWithProbability[containerId.Key] = containerIdsCopy[containerId.Key];
                    }

                // Set desired count to size of array (we want all containers chosen)
                data.ChosenCount = data.ContainerIdsWithProbability.Count;

                // EDGE CASE: chosen container count could be 0
                if (data.ChosenCount == 0)
                {
                    continue;
                }
            }

            // Pass possible containers into function to choose some
            var chosenContainerIds = GetContainersByProbability(key, data);
            foreach (var chosenContainerId in chosenContainerIds)
            {
                // Look up container object from full list of containers on map
                var containerObject = staticRandomisableContainersOnMap.FirstOrDefault(
                    staticContainer => staticContainer.Template.Id == chosenContainerId
                );
                if (containerObject is null)
                {
                    if(_logger.IsLogEnabled(LogLevel.Debug))
                    {
                        _logger.Debug(
                            $"Container: {chosenContainerId} not found in staticRandomisableContainersOnMap, this is bad"
                        );
                    }
                    continue;
                }

                // Add loot to container and push into result object
                var containerWithLoot = AddLootToContainer(
                    containerObject,
                    staticForcedOnMapClone,
                    staticLootDist.Value,
                    staticAmmoDist,
                    locationId
                );
                result.Add(containerWithLoot.Template);
                staticContainerCount++;

                staticLootItemCount += containerWithLoot.Template.Items.Count;
            }
        }

        _logger.Success($"A total of {staticLootItemCount}static items spawned");

        _logger.Success(
            _localisationService.GetText("location-containers_generated_success", staticContainerCount)
        );

        return result;
    }

    /// <summary>
    ///     Get containers with a non-100% chance to spawn OR are NOT on the container type randomistion blacklist
    /// </summary>
    /// <param name="staticContainers"></param>
    /// <returns>StaticContainerData array</returns>
    protected List<StaticContainerData> GetRandomisableContainersOnMap(List<StaticContainerData> staticContainers)
    {
        return staticContainers.Where(
                staticContainer =>
                    staticContainer.Probability != 1 &&
                    !staticContainer.Template.IsAlwaysSpawn.GetValueOrDefault(false) &&
                    !_locationConfig.ContainerRandomisationSettings.ContainerTypesToNotRandomise.Contains(
                        staticContainer.Template.Items[0].Template
                    )
            )
            .ToList();
    }

    /// <summary>
    ///     Get containers with 100% spawn rate or have a type on the randomistion ignore list
    /// </summary>
    /// <param name="staticContainersOnMap"></param>
    /// <returns>IStaticContainerData array</returns>
    protected List<StaticContainerData> GetGuaranteedContainers(List<StaticContainerData> staticContainersOnMap)
    {
        return staticContainersOnMap.Where(
                staticContainer =>
                    staticContainer.Probability == 1 ||
                    staticContainer.Template.IsAlwaysSpawn.GetValueOrDefault(false) ||
                    _locationConfig.ContainerRandomisationSettings.ContainerTypesToNotRandomise.Contains(
                        staticContainer.Template.Items[0].Template
                    )
            )
            .ToList();
    }

    /// <summary>
    ///     Choose a number of containers based on their probabilty value to fulfil the desired count in
    ///     containerData.chosenCount
    /// </summary>
    /// <param name="groupId">Name of the group the containers are being collected for</param>
    /// <param name="containerData">Containers and probability values for a groupId</param>
    /// <returns>List of chosen container Ids</returns>
    protected List<string> GetContainersByProbability(string groupId, ContainerGroupCount containerData)
    {
        var chosenContainerIds = new List<string>();

        var containerIds = containerData.ContainerIdsWithProbability.Keys.ToList();
        if (containerData.ChosenCount > containerIds.Count)
        {
            if(_logger.IsLogEnabled(LogLevel.Debug))
            {
                _logger.Debug(
                    $"Group: {groupId} wants {containerData.ChosenCount} containers but pool only has {containerIds.Count}, add what's available"
                );
            }
            return containerIds;
        }

        // Create probability array with all possible container ids in this group and their relative probability of spawning
        var containerDistribution =
            new ProbabilityObjectArray<ProbabilityObject<string, double>, string, double>(_mathUtil, _cloner, []);
        foreach (var x in containerIds)
        {
            var value = containerData.ContainerIdsWithProbability[x];
            containerDistribution.Add(new ProbabilityObject<string, double>(x, value, value));
        }

        chosenContainerIds.AddRange(containerDistribution.Draw((int)containerData.ChosenCount));

        return chosenContainerIds;
    }

    /// <summary>
    ///     Get a mapping of each groupid and the containers in that group + count of containers to spawn on map
    /// </summary>
    /// <param name="containersGroups">Container group values</param>
    /// <returns>dictionary keyed by groupId</returns>
    protected Dictionary<string, ContainerGroupCount> GetGroupIdToContainerMappings(
        StaticContainer staticContainerGroupData,
        List<StaticContainerData> staticContainersOnMap)
    {
        // Create dictionary of all group ids and choose a count of containers the map will spawn of that group
        var mapping = new Dictionary<string, ContainerGroupCount>();
        foreach (var groupKvP in staticContainerGroupData.ContainersGroups)
        {
            if (!staticContainerGroupData.ContainersGroups.TryGetValue(groupKvP.Key, out var groupData))
            {
                mapping[groupKvP.Key] = new ContainerGroupCount
                {
                    ContainerIdsWithProbability = new Dictionary<string, double>(),
                    ChosenCount = _randomUtil.GetInt(
                        (int)Math.Round(
                            groupData.MinContainers.Value *
                            _locationConfig.ContainerRandomisationSettings.ContainerGroupMinSizeMultiplier
                        ),
                        (int)Math.Round(
                            groupData.MaxContainers.Value *
                            _locationConfig.ContainerRandomisationSettings.ContainerGroupMaxSizeMultiplier
                        )
                    )
                };
            }
        }

        // Add an empty group for containers without a group id but still have a < 100% chance to spawn
        // Likely bad BSG data, will be fixed...eventually, example of the groupids: `NEED_TO_BE_FIXED1`,`NEED_TO_BE_FIXED_SE02`, `NEED_TO_BE_FIXED_NW_01`
        mapping[""] = new ContainerGroupCount { ContainerIdsWithProbability = new Dictionary<string, double>(), ChosenCount = -1 };

        // Iterate over all containers and add to group keyed by groupId
        // Containers without a group go into a group with empty key ""
        foreach (var container in staticContainersOnMap)
        {
            if (!staticContainerGroupData.Containers.TryGetValue(container.Template.Id, out var groupData))
            {
                _logger.Error(
                    _localisationService.GetText(
                        "location-unable_to_find_container_in_statics_json",
                        container.Template.Id
                    )
                );

                continue;
            }

            if (container.Probability >= 1)
            {
                if(_logger.IsLogEnabled(LogLevel.Debug))
                {
                    _logger.Debug(
                        $"Container {container.Template.Id} with group ${groupData.GroupId} had 100 % chance to spawn was picked as random container, skipping"
                    );
                }

                continue;
            }

            mapping.TryAdd(groupData.GroupId, new ContainerGroupCount
            {
                ChosenCount = 0d,
                ContainerIdsWithProbability = new Dictionary<string, double>()
            });
            mapping[groupData.GroupId].ContainerIdsWithProbability.TryAdd(container.Template.Id, container.Probability.Value);
        }

        return mapping;
    }

    /// <summary>
    ///     Choose loot to put into a static container based on weighting
    ///     Handle forced items + seasonal item removal when not in season
    /// </summary>
    /// <param name="staticContainer">The container itself we will add loot to</param>
    /// <param name="staticForced">Loot we need to force into the container</param>
    /// <param name="staticLootDist">staticLoot.json</param>
    /// <param name="staticAmmoDist">staticAmmo.json</param>
    /// <param name="locationName">Name of the map to generate static loot for</param>
    /// <returns>StaticContainerData</returns>
    protected StaticContainerData AddLootToContainer(StaticContainerData staticContainer,
        List<StaticForced>? staticForced,
        Dictionary<string, StaticLootDetails> staticLootDist,
        Dictionary<string, List<StaticAmmoDetails>> staticAmmoDist, string locationName
    )
    {
        var containerClone = _cloner.Clone(staticContainer);
        var containerTpl = containerClone.Template.Items[0].Template;

        // Create new unique parent id to prevent any collisions
        var parentId = _hashUtil.Generate();
        containerClone.Template.Root = parentId;
        containerClone.Template.Items[0].Id = parentId;

        var containerMap = GetContainerMapping(containerTpl);

        // Choose count of items to add to container
        var itemCountToAdd = GetWeightedCountOfContainerItems(containerTpl, staticLootDist, locationName);

        // Get all possible loot items for container
        var containerLootPool = GetPossibleLootItemsForContainer(containerTpl, staticLootDist);

        // Some containers need to have items forced into it (quest keys etc)
        var tplsForced = staticForced
            .Where((forcedStaticProp) => forcedStaticProp.ContainerId == containerClone.Template.Id)
            .Select((x) => x.ItemTpl);

        // Draw random loot
        // Allow money to spawn more than once in container
        var failedToFitCount = 0;
        var locklist = _itemHelper.GetMoneyTpls();

        // Choose items to add to container, factor in weighting + lock money down
        // Filter out items picked that're already in the above `tplsForced` array
        var chosenTpls = containerLootPool
            .Draw(itemCountToAdd, _locationConfig.AllowDuplicateItemsInStaticContainers, locklist)
            .Where((tpl) => !tplsForced.Contains(tpl));

        // Add forced loot to chosen item pool
        var tplsToAddToContainer = tplsForced.Concat(chosenTpls);
        foreach (var tplToAdd in tplsToAddToContainer)
        {
            var chosenItemWithChildren = CreateStaticLootItem(tplToAdd, staticAmmoDist, parentId);
            if (chosenItemWithChildren is null)
            {
                continue;
            }

            var items = _locationConfig.TplsToStripChildItemsFrom.Contains(tplToAdd)
                ? [chosenItemWithChildren.Items[0]] // Strip children from parent
                : chosenItemWithChildren.Items;
            var itemSize = GetItemSize(items);
            var width = (int)itemSize.Width;
            var height = (int)itemSize.Height;

            // look for open slot to put chosen item into
            var result = _containerHelper.FindSlotForItem(containerMap, width, height);
            if (!result.Success.GetValueOrDefault(false))
            {
                if (failedToFitCount > _locationConfig.FitLootIntoContainerAttempts)
                {
                    // x attempts to fit an item, container is probably full, stop trying to add more
                    break;
                }

                // Can't fit item, skip
                failedToFitCount++;

                continue;
            }
            
            _containerHelper.FillContainerMapWithItem(
                containerMap,
                result.X.Value,
                result.Y.Value,
                width,
                height,
                result.Rotation.GetValueOrDefault(false)
            );

            var rotation = result.Rotation.GetValueOrDefault(false) ? 1 : 0;
             
            items[0].SlotId = "main";
            items[0].Location = new ItemLocation{ X = result.X, Y = result.Y, R = rotation };

            // Add loot to container before returning
            containerClone.Template.Items.AddRange(items);
        }

        return containerClone;
    }

    protected ItemSize? GetItemSize(List<Item>? items)
    {
        var rootItem = items[0];
        if (items.Count == 1)
        {
            var itemTemplate = _itemHelper.GetItem(rootItem.Template).Value;
            if (itemTemplate.Properties is null)
            {
                _logger.Error($"Unable to process item: ${{chosenTpl}}. it lacks _props");

                return null;
            }

            // Single item, get items properties
            return new ItemSize
            {
                Width = itemTemplate.Properties.Width.Value,
                Height = itemTemplate.Properties.Height.Value
            };
        }

        
        // Multi-mod-item, use helper to get size of item + attached mods
        var result = _inventoryHelper.GetItemSize(rootItem.Template, rootItem.Id, items);
        return new ItemSize
        {
            Width = result[0],
            Height = result[1]
        };

    }

    /// <summary>
    ///     Get a 2D grid of a container's item slots
    /// </summary>
    /// <param name="containerTpl">Tpl id of the container</param>
    protected int[][] GetContainerMapping(string containerTpl)
    {
        // Get template from db
        var containerTemplate = _itemHelper.GetItem(containerTpl).Value;

        // Get height/width
        var height = containerTemplate.Properties.Grids[0].Props.CellsV;
        var width = containerTemplate.Properties.Grids[0].Props.CellsH;

        return _inventoryHelper.GetBlankContainerMap(height.Value, width.Value);
    }

    /// <summary>
    ///     Look up a containers itemcountDistribution data and choose an item count based on the found weights
    /// </summary>
    /// <param name="containerTypeId">Container to get item count for</param>
    /// <param name="staticLootDist">staticLoot.json</param>
    /// <param name="locationName">Map name (to get per-map multiplier for from config)</param>
    /// <returns>item count</returns>
    protected int GetWeightedCountOfContainerItems(string containerTypeId,
        Dictionary<string, StaticLootDetails> staticLootDist, string locationName)
    {
        // Create probability array to calcualte the total count of lootable items inside container
        var itemCountArray =
            new ProbabilityObjectArray<ProbabilityObject<int, float?>, int, float?>(_mathUtil, _cloner, []);
        var countDistribution = staticLootDist[containerTypeId]?.ItemCountDistribution;
        if (countDistribution is null)
        {
            _logger.Warning(
                _localisationService.GetText(
                    "location-unable_to_find_count_distribution_for_container",
                    new
                    {
                        containerId = containerTypeId, locationName
                    }
                )
            );

            return 0;
        }

        foreach (var itemCountDistribution in countDistribution)
            // Add each count of items into array
            itemCountArray.Add(
                new ProbabilityObject<int, float?>(
                    itemCountDistribution.Count.Value,
                    itemCountDistribution.RelativeProbability.Value,
                    null
                )
            );

        return (int)Math.Round(GetStaticLootMultiplierForLocation(locationName) * itemCountArray.Draw()[0]);
    }

    /// <summary>
    ///     Get all possible loot items that can be placed into a container
    ///     Do not add seasonal items if found + current date is inside seasonal event
    /// </summary>
    /// <param name="containerTypeId">Container to get possible loot for</param>
    /// <param name="staticLootDist">staticLoot.json</param>
    /// <returns>ProbabilityObjectArray of item tpls + probabilty</returns>
    protected ProbabilityObjectArray<ProbabilityObject<string, float?>, string, float?> GetPossibleLootItemsForContainer(string containerTypeId,
        Dictionary<string, StaticLootDetails> staticLootDist) // TODO: Type Fuckery, return type was ProbabilityObjectArray<string, number>
    {
        var seasonalEventActive = _seasonalEventService.SeasonalEventEnabled();
        var seasonalItemTplBlacklist = _seasonalEventService.GetInactiveSeasonalEventItems();

        var itemDistribution =
            new ProbabilityObjectArray<ProbabilityObject<string, float?>, string, float?>(_mathUtil, _cloner, []);

        var itemContainerDistribution = staticLootDist[containerTypeId]?.ItemDistribution;
        if (itemContainerDistribution is null)
        {
            _logger.Warning(_localisationService.GetText("location-missing_item_distribution_data", containerTypeId));

            return itemDistribution;
        }

        foreach (var icd in itemContainerDistribution)
        {
            if (!seasonalEventActive && seasonalItemTplBlacklist.Contains(icd.Tpl))
            {
                // Skip seasonal event items if they're not enabled
                continue;
            }

            // Ensure no blacklisted lootable items are in pool
            if (_itemFilterService.IsLootableItemBlacklisted(icd.Tpl))
            {
                continue;
            }

            itemDistribution.Add(new ProbabilityObject<string, float?>(icd.Tpl, icd.RelativeProbability.Value, null));
        }

        return itemDistribution;
    }

    protected double GetLooseLootMultiplierForLocation(string location)
    {
        return _locationConfig.LooseLootMultiplier[location];
    }

    protected double GetStaticLootMultiplierForLocation(string location)
    {
        return _locationConfig.StaticLootMultiplier[location];
    }

    /// <summary>
    ///     Create array of loose + forced loot using probability system
    /// </summary>
    /// <param name="dynamicLootDist"></param>
    /// <param name="staticAmmoDist"></param>
    /// <param name="locationName">Location to generate loot for</param>
    /// <returns>Array of spawn points with loot in them</returns>
    public List<SpawnpointTemplate> GenerateDynamicLoot(LooseLoot dynamicLootDist,
        Dictionary<string, List<StaticAmmoDetails>> staticAmmoDist,
        string locationName)
    {
        List<SpawnpointTemplate> loot = [];
        List<Spawnpoint> dynamicForcedSpawnPoints = [];

        // Remove christmas items from loot data
        if (!_seasonalEventService.ChristmasEventEnabled())
        {
            dynamicLootDist.Spawnpoints = dynamicLootDist.Spawnpoints.Where(
                    (point) => !point.Template.Id.StartsWith("christmas")
                )
                .ToList();
            dynamicLootDist.SpawnpointsForced = dynamicLootDist.SpawnpointsForced.Where(
                    (point) => !point.Template.Id.StartsWith("christmas")
                )
                .ToList();
        }

        // Build the list of forced loot from both `spawnpointsForced` and any point marked `IsAlwaysSpawn`
        dynamicForcedSpawnPoints.AddRange(dynamicLootDist.SpawnpointsForced);
        dynamicForcedSpawnPoints.AddRange(dynamicLootDist.Spawnpoints.Where((point) => point.Template.IsAlwaysSpawn ?? false));

        // Add forced loot
        AddForcedLoot(loot, dynamicForcedSpawnPoints, locationName, staticAmmoDist);

        var allDynamicSpawnpoints = dynamicLootDist.Spawnpoints;

        // Draw from random distribution
        var desiredSpawnpointCount = Math.Round(
            GetLooseLootMultiplerForLocation(locationName) *
            _randomUtil.GetNormallyDistributedRandomNumber(
                (double)dynamicLootDist.SpawnpointCount.Mean,
                (double)dynamicLootDist.SpawnpointCount.Std
            )
        );

        // Positions not in forced but have 100% chance to spawn
        List<Spawnpoint> guaranteedLoosePoints = [];

        var blacklistedSpawnpoints = _locationConfig.LooseLootBlacklist.GetValueOrDefault(locationName);
        var spawnpointArray = new ProbabilityObjectArray<ProbabilityObject<string, Spawnpoint>, string, Spawnpoint>(_mathUtil, _cloner, []);

        foreach (var spawnpoint in allDynamicSpawnpoints)
        {
            // Point is blacklsited, skip
            if (blacklistedSpawnpoints?.Contains(spawnpoint.Template.Id) ?? false)
            {
                if(_logger.IsLogEnabled(LogLevel.Debug))
                {
                    _logger.Debug($"Ignoring loose loot location: {spawnpoint.Template.Id}");
                }
                continue;
            }

            // We've handled IsAlwaysSpawn above, so skip them
            if (spawnpoint.Template.IsAlwaysSpawn ?? false)
            {
                continue;
            }

            // 100%, add it to guaranteed
            if (spawnpoint.Probability == 1)
            {
                guaranteedLoosePoints.Add(spawnpoint);
                continue;
            }

            spawnpointArray.Add(new ProbabilityObject<string, Spawnpoint>(spawnpoint.Template.Id, spawnpoint.Probability ?? 0, spawnpoint));
        }

        // Select a number of spawn points to add loot to
        // Add ALL loose loot with 100% chance to pool
        List<Spawnpoint> chosenSpawnpoints = [];
        chosenSpawnpoints.AddRange(guaranteedLoosePoints);

        var randomSpawnpointCount = desiredSpawnpointCount - chosenSpawnpoints.Count;
        // Only draw random spawn points if needed
        if (randomSpawnpointCount > 0 && spawnpointArray.Count > 0)
        {
            // Add randomly chosen spawn points
            foreach (var si in spawnpointArray.Draw((int)randomSpawnpointCount, false))
            {
                chosenSpawnpoints.Add(spawnpointArray.Data(si));
            }
        }

        // Filter out duplicate locationIds // prob can be done better
        chosenSpawnpoints = chosenSpawnpoints.GroupBy(spawnpoint => spawnpoint.LocationId).Select(group => group.First()).ToList();

        // Do we have enough items in pool to fulfill requirement
        var tooManySpawnPointsRequested = desiredSpawnpointCount - chosenSpawnpoints.Count > 0;
        if (tooManySpawnPointsRequested)
        {
            if(_logger.IsLogEnabled(LogLevel.Debug))
            {
                _logger.Debug(
                    _localisationService.GetText(
                        "location-spawn_point_count_requested_vs_found",
                        new
                        {
                            requested = desiredSpawnpointCount + guaranteedLoosePoints.Count,
                            found = chosenSpawnpoints.Count,
                            mapName = locationName,
                        }
                    )
                );
            }
        }

        // Iterate over spawnpoints
        var seasonalEventActive = _seasonalEventService.SeasonalEventEnabled();
        var seasonalItemTplBlacklist = _seasonalEventService.GetInactiveSeasonalEventItems();
        foreach (var spawnPoint in chosenSpawnpoints)
        {
            // Spawnpoint is invalid, skip it
            if (spawnPoint.Template is null)
            {
                _logger.Warning(
                    _localisationService.GetText("location-missing_dynamic_template", spawnPoint.LocationId)
                );

                continue;
            }

            // Ensure no blacklisted lootable items are in pool
            spawnPoint.Template.Items = spawnPoint.Template.Items.Where(
                    (item) => !_itemFilterService.IsLootableItemBlacklisted(item.Template)
                )
                .ToList();

            // Ensure no seasonal items are in pool if not in-season
            if (!seasonalEventActive)
            {
                spawnPoint.Template.Items = spawnPoint.Template.Items.Where(
                        (item) => !seasonalItemTplBlacklist.Contains(item.Template)
                    )
                    .ToList();
            }

            // Spawn point has no items after filtering, skip
            if (spawnPoint.Template.Items is null || spawnPoint.Template.Items.Count == 0)
            {
                _logger.Warning(
                    _localisationService.GetText("location-spawnpoint_missing_items", spawnPoint.Template.Id)
                );

                continue;
            }

            // Get an array of allowed IDs after above filtering has occured
            var validItemIds = spawnPoint.Template.Items.Select((item) => item.Id).ToList();

            // Construct container to hold above filtered items, letting us pick an item for the spot
            var itemArray = new ProbabilityObjectArray<ProbabilityObject<string, double?>, string, double?>(_mathUtil, _cloner, []);
            foreach (var itemDist in spawnPoint.ItemDistribution)
            {
                if (!validItemIds.Contains(itemDist.ComposedKey.Key))
                {
                    continue;
                }

                itemArray.Add(new ProbabilityObject<string, double?>(itemDist.ComposedKey.Key, itemDist.RelativeProbability ?? 0, null));
            }

            if (itemArray.Count == 0)
            {
                _logger.Warning(
                    _localisationService.GetText("location-loot_pool_is_empty_skipping", spawnPoint.Template.Id)
                );

                continue;
            }

            // Draw a random item from spawn points possible items
            var chosenComposedKey = itemArray.Draw(1).FirstOrDefault();
            var createItemResult = CreateDynamicLootItem(
                chosenComposedKey,
                spawnPoint.Template.Items,
                staticAmmoDist
            );

            // Root id can change when generating a weapon, ensure ids match
            spawnPoint.Template.Root = createItemResult.Items.FirstOrDefault().Id;

            // Overwrite entire pool with chosen item
            spawnPoint.Template.Items = createItemResult.Items;

            loot.Add(spawnPoint.Template);
        }

        return loot;
    }

    /// <summary>
    ///     Add forced spawn point loot into loot parameter list
    /// </summary>
    /// <param name="lootLocationTemplates">List to add forced loot spawn locations to</param>
    /// <param name="forcedSpawnPoints">Forced loot locations that must be added</param>
    /// <param name="locationName">Name of map currently having force loot created for</param>
    protected void AddForcedLoot(List<SpawnpointTemplate> lootLocationTemplates,
        List<Spawnpoint> forcedSpawnPoints, string locationName,
        Dictionary<string, List<StaticAmmoDetails>> staticAmmoDist)
    {
        var lootToForceSingleAmountOnMap = _locationConfig.ForcedLootSingleSpawnById.GetValueOrDefault(locationName);
        if (lootToForceSingleAmountOnMap is not null)
        {
            // Process loot items defined as requiring only 1 spawn position as they appear in multiple positions on the map
            foreach (var itemTpl in lootToForceSingleAmountOnMap)
            {
                // Get all spawn positions for item tpl in forced loot array
                var items = forcedSpawnPoints.Where(
                    (forcedSpawnPoint) => forcedSpawnPoint.Template.Items[0].Template == itemTpl
                );
                if (items is null || !items.Any())
                {
                    if(_logger.IsLogEnabled(LogLevel.Debug))
                    {
                        _logger.Debug($"Unable to adjust loot item {itemTpl} as it does not exist inside {locationName} forced loot.");
                    }
                    continue;
                }

                // Create probability array of all spawn positions for this spawn id
                var spawnpointArray = new ProbabilityObjectArray<ProbabilityObject<string, Spawnpoint>, string, Spawnpoint>(_mathUtil, _cloner, []);
                foreach (var si in items)
                {
                    // use locationId as template.Id is the same across all items
                    spawnpointArray.Add(new ProbabilityObject<string, Spawnpoint>(si.LocationId, si.Probability ?? 0, si));
                }

                // Choose 1 out of all found spawn positions for spawn id and add to loot array
                foreach (var spawnPointLocationId in spawnpointArray.Draw(1, false))
                {
                    var itemToAdd = items.FirstOrDefault((item) => item.LocationId == spawnPointLocationId);
                    var lootItem = itemToAdd?.Template;
                    if (lootItem is null)
                    {
                        _logger.Warning($"Item with spawn point id {spawnPointLocationId} could not be found, skipping");
                        continue;
                    }

                    var createItemResult = CreateDynamicLootItem(
                        lootItem.Items.FirstOrDefault().Id,
                        lootItem.Items,
                        staticAmmoDist
                    );

                    // Update root ID with the dynamically generated ID
                    lootItem.Root = createItemResult.Items.FirstOrDefault().Id;
                    lootItem.Items = createItemResult.Items;
                    lootLocationTemplates.Add(lootItem);
                }
            }
        }

        var seasonalEventActive = _seasonalEventService.SeasonalEventEnabled();
        var seasonalItemTplBlacklist = _seasonalEventService.GetInactiveSeasonalEventItems();

        // Add remaining forced loot to array
        foreach (var forcedLootLocation in forcedSpawnPoints)
        {
            var firstLootItemTpl = forcedLootLocation.Template.Items.FirstOrDefault().Template;

            // Skip spawn positions processed already
            if (lootToForceSingleAmountOnMap?.Contains(firstLootItemTpl) ?? false)
            {
                continue;
            }

            // Skip adding seasonal items when seasonal event is not active
            if (!seasonalEventActive && seasonalItemTplBlacklist.Contains(firstLootItemTpl))
            {
                continue;
            }

            var locationTemplateToAdd = forcedLootLocation.Template;
            var createItemResult = CreateDynamicLootItem(
                locationTemplateToAdd.Items.FirstOrDefault().Id,
                forcedLootLocation.Template.Items,
                staticAmmoDist
            );

            // Update root ID with the dynamically generated ID
            forcedLootLocation.Template.Root = createItemResult.Items.FirstOrDefault().Id;
            forcedLootLocation.Template.Items = createItemResult.Items;

            // Push forced location into array as long as it doesnt exist already
            var existingLocation = lootLocationTemplates.Any(
                (spawnPoint) => spawnPoint.Id == locationTemplateToAdd.Id
            );
            if (!existingLocation)
            {
                lootLocationTemplates.Add(locationTemplateToAdd);
            }
            else
            {
                if(_logger.IsLogEnabled(LogLevel.Debug))
                {
                    _logger.Debug(
                        $"Attempted to add a forced loot location with Id: {locationTemplateToAdd.Id} to map {locationName} that already has that id in use, skipping"
                    );
                }
            }
        }
    }

    private ContainerItem CreateDynamicLootItem(string? chosenComposedKey, List<Item> items, Dictionary<string, List<StaticAmmoDetails>> staticAmmoDist)
    {
        var chosenItem = items.FirstOrDefault((item) => item.Id == chosenComposedKey);
        var chosenTpl = chosenItem?.Template;
        if (chosenTpl is null) {
            throw new Exception($"Item for tpl {chosenComposedKey} was not found in the spawn point");
        }
        var itemTemplate = _itemHelper.GetItem(chosenTpl).Value;
        if (itemTemplate is null) {
            _logger.Error($"Item tpl: {chosenTpl} cannot be found in database");
        }

        // Item array to return
        List<Item> itemWithMods = [];

        // Money/Ammo - don't rely on items in spawnPoint.template.Items so we can randomise it ourselves
        if (_itemHelper.IsOfBaseclasses(chosenTpl, [BaseClasses.MONEY, BaseClasses.AMMO])) {
            var stackCount =
                itemTemplate.Properties.StackMaxSize == 1
                    ? 1
                    : _randomUtil.GetInt((int)itemTemplate.Properties.StackMinRandom, (int)itemTemplate.Properties.StackMaxRandom);

            itemWithMods.Add(new Item {
                Id = _hashUtil.Generate(),
                Template = chosenTpl,
                Upd = new Upd { StackObjectsCount = stackCount }
            });
        } else if (_itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.AMMO_BOX)) {
            // Fill with cartridges
            List<Item> ammoBoxItem = [ new Item { Id = _hashUtil.Generate(), Template = chosenTpl }];
            _itemHelper.AddCartridgesToAmmoBox(ammoBoxItem, itemTemplate);
            itemWithMods.AddRange(ammoBoxItem);
        } else if (_itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.MAGAZINE)) {
            // Create array with just magazine
            List<Item> magazineItem = [new Item { Id = _hashUtil.Generate(), Template = chosenTpl }];

            if (_randomUtil.GetChance100(_locationConfig.StaticMagazineLootHasAmmoChancePercent)) {
                // Add randomised amount of cartridges
                _itemHelper.FillMagazineWithRandomCartridge(
                    magazineItem,
                    itemTemplate, // Magazine template
                    staticAmmoDist,
                    null,
                    _locationConfig.MinFillLooseMagazinePercent / 100
                );
            }

            itemWithMods.AddRange(magazineItem);
        } else {
            // Also used by armors to get child mods
            // Get item + children and add into array we return
            var itemWithChildren = _itemHelper.FindAndReturnChildrenAsItems(items, chosenItem.Id);

            // Ensure all IDs are unique
            itemWithChildren = _itemHelper.ReplaceIDs(_cloner.Clone(itemWithChildren));

            if (_locationConfig.TplsToStripChildItemsFrom.Contains(chosenItem.Template)) {
                // Strip children from parent before adding
                itemWithChildren = [itemWithChildren[0]];
            }

            itemWithMods.AddRange(itemWithChildren);
        }

        // Get inventory size of item
        var size = _itemHelper.GetItemSize(itemWithMods, itemWithMods[0].Id);

        return new ContainerItem { Items = itemWithMods, Width = size.Width, Height = size.Height };
    }

    private double GetLooseLootMultiplerForLocation(string location)
    {
        return _locationConfig.LooseLootMultiplier[location];
    }
    
    protected double getStaticLootMultiplerForLocation(string location) {
        return _locationConfig.StaticLootMultiplier[location];
    }


    // TODO: rewrite, BIG yikes
    protected ContainerItem? CreateStaticLootItem(
        string chosenTpl,
        Dictionary<string, List<StaticAmmoDetails>> staticAmmoDist,
        string? parentId = null)
    {
        var itemTemplate = _itemHelper.GetItem(chosenTpl).Value;
        if (itemTemplate.Properties is null)
        {
            _logger.Error($"Unable to process item: ${{chosenTpl}}. it lacks _props");

            return null;
        }

        var width = itemTemplate.Properties.Width;
        var height = itemTemplate.Properties.Height;
        List<Item> items = [new Item { Id = _hashUtil.Generate(), Template = chosenTpl }];
        var rootItem = items.FirstOrDefault();

        // Use passed in parentId as override for new item
        if (!string.IsNullOrEmpty(parentId))
        {
            rootItem.ParentId = parentId;
        }

        if (
            _itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.MONEY) ||
            _itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.AMMO)
        )
        {
            // Edge case - some ammos e.g. flares or M406 grenades shouldn't be stacked
            var stackCount = itemTemplate.Properties.StackMaxSize == 1
                ? 1
                : _randomUtil.GetInt((int)(itemTemplate.Properties.StackMinRandom), (int)(itemTemplate.Properties.StackMaxRandom));

            rootItem.Upd = new Upd { StackObjectsCount = stackCount };
        }
        // No spawn point, use default template
        else if (_itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.WEAPON))
        {
            List<Item> children = [];
            var defaultPreset = _cloner.Clone(_presetHelper.GetDefaultPreset(chosenTpl));
            if (defaultPreset?.Items is not null)
            {
                try
                {
                    children = _itemHelper.ReparentItemAndChildren(defaultPreset.Items[0], defaultPreset.Items);
                }
                catch (Exception e)
                {
                    // this item already broke it once without being reproducible tpl = "5839a40f24597726f856b511"; AKS-74UB Default
                    // 5ea03f7400685063ec28bfa8 // ppsh default
                    // 5ba26383d4351e00334c93d9 //mp7_devgru
                    _logger.Error(
                        _localisationService.GetText(
                            "location-preset_not_found",
                            new
                            {
                                tpl = chosenTpl,
                                defaultId = defaultPreset.Id,
                                defaultName = defaultPreset.Name,
                                parentId,
                            }
                        )
                    );

                    throw;
                }
            }
            else
            {
                // RSP30 (62178be9d0050232da3485d9/624c0b3340357b5f566e8766/6217726288ed9f0845317459) doesn't have any default presets and kills this code below as it has no chidren to reparent
                if(_logger.IsLogEnabled(LogLevel.Debug))
                {
                    _logger.Debug($"createStaticLootItem() No preset found for weapon: {chosenTpl}");
                }
            }

            rootItem = items[0];
            if (rootItem is null)
            {
                _logger.Error(
                    _localisationService.GetText(
                        "location-missing_root_item",
                        new
                        {
                            tpl = chosenTpl,
                            parentId,
                        }
                    )
                );

                throw new Exception(_localisationService.GetText("location-critical_error_see_log"));
            }

            try
            {
                if (children?.Count > 0)
                {
                    items = _itemHelper.ReparentItemAndChildren(rootItem, children);
                }
            }
            catch (Exception e)
            {
                _logger.Error(
                    _localisationService.GetText(
                        "location-unable_to_reparent_item",
                        new
                        {
                            tpl = chosenTpl,
                            parentId = parentId,
                        }
                    )
                );

                throw;
            }

            // Here we should use generalized BotGenerators functions e.g. fillExistingMagazines in the future since
            // it can handle revolver ammo (it's not restructured to be used here yet.)
            // General: Make a WeaponController for Ragfair preset stuff and the generating weapons and ammo stuff from
            // BotGenerator
            var magazine = items.FirstOrDefault(item => item.SlotId == "mod_magazine");
            // some weapon presets come without magazine; only fill the mag if it exists
            if (magazine is not null)
            {
                var magTemplate = _itemHelper.GetItem(magazine.Template).Value;
                var weaponTemplate = _itemHelper.GetItem(chosenTpl).Value;

                // Create array with just magazine
                var defaultWeapon = _itemHelper.GetItem(rootItem.Template).Value;
                List<Item> magazineWithCartridges = [magazine];
                _itemHelper.FillMagazineWithRandomCartridge(
                    magazineWithCartridges,
                    magTemplate,
                    staticAmmoDist,
                    weaponTemplate.Properties.AmmoCaliber,
                    0.25,
                    defaultWeapon.Properties.DefAmmo,
                    defaultWeapon
                );

                // Replace existing magazine with above array
                items.Remove(magazine);
                items.AddRange(magazineWithCartridges);
            }

            var size = _itemHelper.GetItemSize(items, rootItem.Id);
            width = size.Width;
            height = size.Height;
        }
        // No spawnpoint to fall back on, generate manually
        else if (_itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.AMMO_BOX))
        {
            _itemHelper.AddCartridgesToAmmoBox(items, itemTemplate);
        }
        else if (_itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.MAGAZINE))
        {
            if (_randomUtil.GetChance100(_locationConfig.MagazineLootHasAmmoChancePercent))
            {
                // Create array with just magazine
                List<Item> magazineWithCartridges = [rootItem];
                _itemHelper.FillMagazineWithRandomCartridge(
                    magazineWithCartridges,
                    itemTemplate,
                    staticAmmoDist,
                    null,
                    _locationConfig.MinFillStaticMagazinePercent / 100
                );

                // Replace existing magazine with above array
                items.Remove(rootItem);
                items.AddRange(magazineWithCartridges);
            }
        }
        else if (_itemHelper.ArmorItemCanHoldMods(chosenTpl))
        {
            var defaultPreset = _presetHelper.GetDefaultPreset(chosenTpl);
            if (defaultPreset is not null)
            {
                List<Item> presetAndMods = _itemHelper.ReplaceIDs(_cloner.Clone(defaultPreset.Items));
                _itemHelper.RemapRootItemId(presetAndMods);

                // Use original items parentId otherwise item doesnt get added to container correctly
                presetAndMods[0].ParentId = rootItem.ParentId;
                items = presetAndMods;
            }
            else
            {
                // We make base item above, at start of function, no need to do it here
                if ((itemTemplate.Properties.Slots?.Count ?? 0) > 0)
                {
                    items = _itemHelper.AddChildSlotItems(
                        items,
                        itemTemplate,
                        _locationConfig.EquipmentLootSettings.ModSpawnChancePercent
                    );
                }
            }
        }

        return new ContainerItem { Items = items, Width = width, Height = height };
    }
}

public class ContainerGroupCount
{
    [JsonPropertyName("containerIdsWithProbability")]
    public Dictionary<string, double>? ContainerIdsWithProbability { get; set; }

    [JsonPropertyName("chosenCount")]
    public double? ChosenCount { get; set; }
}

public class ContainerItem
{
    [JsonPropertyName("items")]
    public List<Item>? Items { get; set; }

    [JsonPropertyName("width")]
    public double? Width { get; set; }

    [JsonPropertyName("height")]
    public double? Height { get; set; }
}
