using LootDumpProcessor.Logger;
using LootDumpProcessor.Model;
using LootDumpProcessor.Model.Output;
using LootDumpProcessor.Model.Output.LooseLoot;
using LootDumpProcessor.Model.Processing;
using LootDumpProcessor.Storage;
using LootDumpProcessor.Storage.Collections;
using LootDumpProcessor.Utils;
using NumSharp;

namespace LootDumpProcessor.Process.Processor;

public static class LooseLootProcessor
{
    public static PreProcessedLooseLoot PreProcessLooseLoot(List<Template> looseloot)
    {
        var looseloot_ci = new PreProcessedLooseLoot
        {
            Counts = new Dictionary<string, int>()
        };
        var temporalItemProperties = new SubdivisionedKeyableDictionary<string, List<Template>>();
        looseloot_ci.ItemProperties = (AbstractKey)temporalItemProperties.GetKey();
        looseloot_ci.MapSpawnpointCount = looseloot.Count;
        var uniqueIds = new Dictionary<string, object>();
        // sometimes the rotation changes very slightly in the dumps for the same location / rotation spawnpoint
        // use rounding to make sure it is not generated to two spawnpoint

        foreach (var looseLootTemplate in looseloot)
        {
            // the bsg ids are insane.
            // Sometimes the last 7 digits vary but they spawn the same item at the same position
            // e.g. for the quest item "60a3b65c27adf161da7b6e14" at "loot_bunker_quest (3)555192"
            // so the first approach was to remove the last digits.
            // We then saw, that sometimes when the last digits differ for the same string, also the position
            // differs.
            // We decided to group over the position/rotation/useGravity since they make out a distinct spot
            var saneId = looseLootTemplate.GetSaneId();
            if (!uniqueIds.ContainsKey(saneId))
            {
                uniqueIds[saneId] = looseLootTemplate.Id;
                if (looseloot_ci.Counts.ContainsKey(saneId))
                    looseloot_ci.Counts[saneId]++;
                else
                    looseloot_ci.Counts[saneId] = 1;
            }

            if (!temporalItemProperties.TryGetValue(saneId, out var templates))
            {
                templates = new FlatKeyableList<Template>();
                temporalItemProperties.Add(saneId, templates);
            }

            templates.Add(looseLootTemplate);
        }

        DataStorageFactory.GetInstance().Store(temporalItemProperties);
        return looseloot_ci;
    }

    public static Dictionary<string, LooseLootRoot> CreateLooseLootDistribution(
        Dictionary<string, int> map_counts,
        Dictionary<string, IKey> looseloot_counts
    )
    {
        var forcedConfidence = LootDumpProcessorContext.GetConfig().ProcessorConfig.SpawnPointToleranceForForced / 100;
        var probabilities = new Dictionary<string, Dictionary<string, double>>();
        var looseLootDistribution = new Dictionary<string, LooseLootRoot>();
        foreach (var _tup_1 in map_counts)
        {
            var mapName = _tup_1.Key;
            var mapCount = _tup_1.Value;
            probabilities[mapName] = new Dictionary<string, double>();
            var looseLootCounts = DataStorageFactory.GetInstance().GetItem<LooseLootCounts>(looseloot_counts[mapName]);

            var counts = DataStorageFactory.GetInstance()
                .GetItem<FlatKeyableDictionary<string, int>>(looseLootCounts.Counts);
            foreach (var (idi, cnt) in counts)
            {
                probabilities[mapName][idi] = (double)((decimal)cnt / mapCount);
            }

            // No longer used, dispose
            counts = null;

            // we want to cleanup the data, so we calculate the mean for the values we get raw
            // For whatever reason, we sometimes get dumps that have A LOT more loose loot point than 
            // the average
            var initialMean = np.mean(np.array(looseLootCounts.MapSpawnpointCount)).ToArray<double>().First();
            var looseLootCountTolerancePercentage = LootDumpProcessorContext.GetConfig().ProcessorConfig.LooseLootCountTolerancePercentage / 100;
            // We calculate here a high point to check, anything above this value will be ignored
            // The data that was inside those loose loot points still counts for them though!
            var high = initialMean * (1 + looseLootCountTolerancePercentage);
            looseLootCounts.MapSpawnpointCount = looseLootCounts.MapSpawnpointCount.Where(v => v <= high).ToList();

            looseLootDistribution[mapName] = new LooseLootRoot
            {
                SpawnPointCount = new SpawnPointCount
                {
                    Mean = np.mean(np.array(looseLootCounts.MapSpawnpointCount)),
                    Std = np.std(np.array(looseLootCounts.MapSpawnpointCount))
                },
                SpawnPointsForced = new List<SpawnPointsForced>(),
                SpawnPoints = new List<SpawnPoint>()
            };

            var itemProperties = DataStorageFactory.GetInstance()
                .GetItem<FlatKeyableDictionary<string, IKey>>(looseLootCounts.ItemProperties);
            foreach (var (spawnPoint, itemList) in itemProperties)
            {
                var itemsCounts = new Dictionary<ComposedKey, int>();

                var savedItemProperties = DataStorageFactory.GetInstance().GetItem<FlatKeyableList<Template>>(itemList);
                foreach (var savedTemplateProperties in savedItemProperties)
                {
                    var key = new ComposedKey(savedTemplateProperties);
                    if (itemsCounts.ContainsKey(key))
                        itemsCounts[key] += 1;
                    else
                        itemsCounts[key] = 1;
                }

                // Group by arguments to create possible positions / rotations per spawnpoint

                // check if grouping is unique
                var itemListSorted = savedItemProperties.Select(template => (template.GetSaneId(), template))
                    .GroupBy(g => g.Item1).ToList();

                if (itemListSorted.Count > 1)
                {
                    throw new Exception("More than one saneKey found");
                }

                var spawnPoints = itemListSorted.First().Select(v => v.template).ToList();
                var locationId = spawnPoints[0].GetLocationId();
                var template = ProcessorUtil.Copy(spawnPoints[0]);
                //template.Root = null; // Why do we do this, not null in bsg data
                var itemDistribution = itemsCounts.Select(kv => new ItemDistribution
                {
                    ComposedKey = kv.Key,
                    RelativeProbability = kv.Value
                }).ToList();

                // If any of the items is a quest item or forced loose loot items, or the item normally appreas 99.5%
                // Only add position to forced loot if it has only 1 item in the array.
                if (itemDistribution.Count == 1 && itemDistribution.Any(item =>
                        LootDumpProcessorContext.GetTarkovItems().IsQuestItem(item.ComposedKey?.FirstItem?.Tpl) ||
                        LootDumpProcessorContext.GetForcedLooseItems()[mapName].Contains(item.ComposedKey?.FirstItem?.Tpl))
                   )
                {
                    var spawnPointToAdd = new SpawnPointsForced
                    {
                        LocationId = locationId,
                        Probability = probabilities[mapName][spawnPoint],
                        Template = template
                    };
                    looseLootDistribution[mapName].SpawnPointsForced.Add(spawnPointToAdd);
                }
                else if (probabilities[mapName][spawnPoint] > forcedConfidence)
                {
                    var spawnPointToAdd = new SpawnPointsForced
                    {
                        LocationId = locationId,
                        Probability = probabilities[mapName][spawnPoint],
                        Template = template
                    };
                    looseLootDistribution[mapName].SpawnPointsForced.Add(spawnPointToAdd);
                    if (LoggerFactory.GetInstance().CanBeLogged(LogLevel.Warning))
                        LoggerFactory.GetInstance().Log(
                            $"Item: {template.Id} has > {LootDumpProcessorContext.GetConfig().ProcessorConfig.SpawnPointToleranceForForced}% spawn chance in spawn point: {spawnPointToAdd.LocationId} but isn't in forced loot, adding to forced",
                            LogLevel.Warning
                        );
                }
                else // Normal spawn point, add to non-forced spawnpoint array
                {
                    var spawnPointToAdd = new SpawnPoint
                    {
                        LocationId = locationId,
                        Probability = probabilities[mapName][spawnPoint],
                        Template = template,
                        ItemDistribution = itemDistribution
                    };
                    
                    template.Items = new List<Item>();

                    var group = spawnPoints.GroupBy(template => new ComposedKey(template))
                        .ToDictionary(g => g.Key, g => g.ToList());
                    foreach (var distribution in itemDistribution)
                    {
                        if (group.TryGetValue(distribution.ComposedKey, out var items))
                        {
                            // We need to reparent the IDs to match the composed key ID
                            var itemDistributionItemList = items.First().Items;
                            // Find the item with no parent id, this is essentially the "Root" of the actual item
                            var firstItemInTemplate =
                                itemDistributionItemList.Find(i => string.IsNullOrEmpty(i.ParentId));
                            // Save the original ID reference, we need to replace it on child items
                            var originalId = firstItemInTemplate.Id;
                            // Put the composed key instead
                            firstItemInTemplate.Id = distribution.ComposedKey.Key;
                            // Reparent any items with the original id on it
                            itemDistributionItemList.Where(i => i.ParentId == originalId)
                                .ToList()
                                .ForEach(i => i.ParentId = firstItemInTemplate.Id);
                            
                            template.Items.AddRange(itemDistributionItemList);
                        }
                        else
                        {
                            if (LoggerFactory.GetInstance().CanBeLogged(LogLevel.Error))
                                LoggerFactory.GetInstance().Log(
                                    $"Item template {distribution.ComposedKey?.FirstItem?.Tpl} was on loose loot distribution for spawn point {template.Id} but the spawn points didnt contain a template matching it.",
                                    LogLevel.Error
                                );
                        }
                    }

                    looseLootDistribution[mapName].SpawnPoints.Add(spawnPointToAdd);
                }
            }

            // # Test for duplicate position
            // # we removed most of them by "rounding away" the jitter in rotation,
            // # there are still a few duplicate locations with distinct difference in rotation left though
            // group_fun = lambda x: (
            //     x["template"]["Position"]["x"],
            //     x["template"]["Position"]["y"],
            //     x["template"]["Position"]["z"],
            // )
            // test = sorted(loose_loot_distribution[mi]["spawnpoints"], key=group_fun)
            // test_grouped = groupby(test, group_fun)
            // test_len = []
            // for k, g in test_grouped:
            //     gl = list(g)
            //     test_len.append(len(gl))
            //     if len(gl) > 1:
            //         print(gl)
            //
            // print(mi, np.unique(test_len, return_counts=True))
            looseLootDistribution[mapName].SpawnPoints =
                looseLootDistribution[mapName].SpawnPoints.OrderBy(x => x.Template.Id).ToList();
            // Cross check with forced loot in dumps vs items defined in forced_loose.yaml
            var forcedTplsInConfig = new HashSet<string>(
                (from forceditem in LootDumpProcessorContext.GetForcedLooseItems()[mapName]
                    select forceditem).ToList());
            var forcedTplsFound = new HashSet<string>(
                (from forceditem in looseLootDistribution[mapName].SpawnPointsForced
                    select forceditem.Template.Items[0].Tpl).ToList());

            // All the tpls that are defined in the forced_loose.yaml for this map that are not found as forced
            foreach (var itemTpl in forcedTplsInConfig)
            {
                if (!forcedTplsFound.Contains(itemTpl))
                {
                    if (LoggerFactory.GetInstance().CanBeLogged(LogLevel.Error))
                        LoggerFactory.GetInstance().Log(
                            $"Expected item: {itemTpl} defined in forced_loose.yaml config not found in forced loot",
                            LogLevel.Error
                        );
                }
            }

            // All the tpls that are found as forced in output file but not in the forced_loose.yaml config
            foreach (var itemTpl in forcedTplsFound)
            {
                if (!forcedTplsInConfig.Contains(itemTpl))
                {
                    if (LoggerFactory.GetInstance().CanBeLogged(LogLevel.Warning))
                        LoggerFactory.GetInstance().Log(
                            $"Map: {mapName} Item: {itemTpl} not defined in forced_loose.yaml config but was flagged as forced by code",
                            LogLevel.Warning
                        );
                }
            }
        }

        return looseLootDistribution;
    }
}