using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Options;
using SpaceGame.Api.Shared.Runtime;
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;

namespace SpaceGame.Api.Universe.Bootstrap;

public sealed class StaticDataProvider : IStaticDataProvider
{
    private const string MilitaryShipCategory = "military";
    private const string ConstructionShipCategory = "construction";
    private const string TransportShipCategory = "transport";
    private const string MiningShipCategory = "mining";
    private readonly string _dataRoot;
    private readonly JsonSerializerOptions _jsonOptions = new()
    {
        PropertyNameCaseInsensitive = true,
        Converters = { new JsonStringEnumConverter() },
    };

    public StaticDataProvider(IOptions<StaticDataOptions> staticDataOptions)
    {
        _dataRoot = staticDataOptions.Value.DataRoot;

        var knownSystems = Read<List<SolarSystemDefinition>>("systems.json");
        var races = Read<List<RaceDefinition>>("races.json");
        var factions = Read<List<FactionDefinition>>("factions.json");
        var modules = NormalizeModules(Read<List<ModuleDefinition>>("modules.json"));
        var ships = Read<List<ShipDefinition>>("ships.json");
        var items = Read<List<ItemDefinition>>("items.json");
        var recipes = BuildRecipes(items, ships, modules);
        var moduleRecipes = BuildModuleRecipes(modules);

        KnownSystems = knownSystems;
        RaceDefinitions = races.ToDictionary(definition => definition.Id, StringComparer.Ordinal);
        FactionDefinitions = factions.ToDictionary(definition => definition.Id, StringComparer.Ordinal);
        ModuleDefinitions = modules.ToDictionary(definition => definition.Id, StringComparer.Ordinal);
        ShipDefinitions = ships.ToDictionary(definition => definition.Id, StringComparer.Ordinal);
        ItemDefinitions = items.ToDictionary(definition => definition.Id, StringComparer.Ordinal);
        Recipes = recipes.ToDictionary(definition => definition.Id, StringComparer.Ordinal);
        ModuleRecipes = moduleRecipes.ToDictionary(definition => definition.ModuleId, StringComparer.Ordinal);
        ProductionGraph = ProductionGraphBuilder.Build(items, recipes, modules);
    }

    public IReadOnlyList<SolarSystemDefinition> KnownSystems { get; }

    public IReadOnlyDictionary<string, RaceDefinition> RaceDefinitions { get; }

    public IReadOnlyDictionary<string, FactionDefinition> FactionDefinitions { get; }

    public IReadOnlyDictionary<string, ModuleDefinition> ModuleDefinitions { get; }

    public IReadOnlyDictionary<string, ShipDefinition> ShipDefinitions { get; }

    public IReadOnlyDictionary<string, ItemDefinition> ItemDefinitions { get; }

    public IReadOnlyDictionary<string, RecipeDefinition> Recipes { get; }

    public IReadOnlyDictionary<string, ModuleRecipeDefinition> ModuleRecipes { get; }

    public ProductionGraph ProductionGraph { get; }

    private T Read<T>(string fileName)
    {
        var path = Path.Combine(_dataRoot, fileName);
        var json = File.ReadAllText(path);
        return JsonSerializer.Deserialize<T>(json, _jsonOptions)
          ?? throw new InvalidOperationException($"Unable to read {fileName}.");
    }

    private static List<ModuleRecipeDefinition> BuildModuleRecipes(IEnumerable<ModuleDefinition> modules) =>
      modules
        .Where(module => module.BuildRecipes.Count > 0)
        .Select(module => new ModuleRecipeDefinition
        {
            ModuleId = module.Id,
            Duration = module.BuildRecipes[0].Time,
            Inputs = module.BuildRecipes[0].Wares
            .Select(input => new RecipeInputDefinition
            {
                ItemId = input.ItemId,
                Amount = input.Amount,
            })
            .ToList(),
        })
        .ToList();

    private static List<RecipeDefinition> BuildRecipes(IEnumerable<ItemDefinition> items, IEnumerable<ShipDefinition> ships, IReadOnlyCollection<ModuleDefinition> modules)
    {
        var recipes = new List<RecipeDefinition>();
        var preferredProducerByItemId = modules
          .Where(module => module.ProductItemIds.Count > 0)
          .GroupBy(module => module.ProductItemIds[0], StringComparer.Ordinal)
          .ToDictionary(
            group => group.Key,
            group => group.OrderBy(module => module.Id, StringComparer.Ordinal).First().Id,
            StringComparer.Ordinal);

        foreach (var item in items)
        {
            if (item.Production.Count > 0)
            {
                foreach (var production in item.Production)
                {
                    recipes.Add(new RecipeDefinition
                    {
                        Id = $"{item.Id}-{production.Method}-production",
                        Label = production.Name == "Universal" ? item.Name : $"{item.Name} ({production.Name})",
                        FacilityCategory = InferFacilityCategory(item),
                        Duration = production.Time,
                        Priority = InferRecipePriority(item),
                        RequiredModules = InferRequiredModules(item, preferredProducerByItemId),
                        Inputs = production.Wares
                        .Select(input => new RecipeInputDefinition
                        {
                            ItemId = input.ItemId,
                            Amount = input.Amount,
                        })
                        .ToList(),
                        Outputs =
                      [
                        new RecipeOutputDefinition
                        {
                            ItemId = item.Id,
                            Amount = production.Amount,
                        },
                      ],
                    });
                }

                continue;
            }

            if (item.Construction is null)
            {
                continue;
            }

            recipes.Add(new RecipeDefinition
            {
                Id = item.Construction.RecipeId ?? $"{item.Id}-production",
                Label = item.Name,
                FacilityCategory = item.Construction.FacilityCategory,
                Duration = item.Construction.CycleTime,
                Priority = item.Construction.Priority,
                RequiredModules = item.Construction.RequiredModules.ToList(),
                Inputs = item.Construction.Requirements
                .Select(input => new RecipeInputDefinition
                {
                    ItemId = input.ItemId,
                    Amount = input.Amount,
                })
                .ToList(),
                Outputs =
              [
                new RecipeOutputDefinition
                {
                    ItemId = item.Id,
                    Amount = item.Construction.BatchSize,
                },
              ],
            });
        }

        foreach (var ship in ships)
        {
            foreach (var production in ship.Production)
            {
                recipes.Add(new RecipeDefinition
                {
                    Id = $"{ship.Id}-{production.Method}-construction",
                    Label = $"{ship.Name} Construction",
                    FacilityCategory = "shipyard",
                    Duration = production.Time,
                    Priority = InferShipRecipePriority(ship),
                    RequiredModules = InferShipBuildModules(ship),
                    Inputs = production.Wares
                    .Select(input => new RecipeInputDefinition
                    {
                        ItemId = input.ItemId,
                        Amount = input.Amount,
                    })
                    .ToList(),
                    ShipOutputId = ship.Id,
                });
            }
        }

        return recipes;
    }

    private static string InferFacilityCategory(ItemDefinition item) =>
      item.Group switch
      {
          "agricultural" or "food" or "pharmaceutical" or "water" => "farm",
          _ => "station",
      };

    private static List<string> InferRequiredModules(ItemDefinition item, IReadOnlyDictionary<string, string> preferredProducerByItemId)
    {
        if (preferredProducerByItemId.TryGetValue(item.Id, out var moduleId))
        {
            return [moduleId];
        }

        return [];
    }

    private static int InferRecipePriority(ItemDefinition item) =>
      item.Group switch
      {
          "energy" => 140,
          "water" => 130,
          "food" => 120,
          "agricultural" => 110,
          "refined" => 100,
          "hightech" => 90,
          "shiptech" => 80,
          "pharmaceutical" => 70,
          _ => 60,
      };

    private static List<string> InferShipBuildModules(ShipDefinition ship) =>
      ship.Size switch
      {
          "extrasmall" or "small" or "medium" => ["module_gen_build_dockarea_m_01"],
          "large" => ["module_gen_build_l_01"],
          "extralarge" => ["module_gen_build_xl_01"],
          _ => ["module_gen_build_dockarea_m_01"],
      };

    private static int InferShipRecipePriority(ShipDefinition ship) =>
      GetShipCategory(ship) switch
      {
          MilitaryShipCategory => 170,
          ConstructionShipCategory => 140,
          TransportShipCategory => 120,
          MiningShipCategory => 110,
          _ => 100,
      };

    private static List<ModuleDefinition> NormalizeModules(List<ModuleDefinition> modules)
    {
        for (var index = 0; index < modules.Count; index += 1)
        {
            var module = modules[index];
            try
            {
                module.ModuleType = module.Type.ToModuleType();
            }
            catch (ArgumentOutOfRangeException exception)
            {
                throw new InvalidOperationException($"Module '{module.Id}' has unsupported type '{module.Type}'.", exception);
            }

            module.Type = module.ModuleType.ToDataValue();
            modules[index] = CreateSpecializedModuleDefinition(module);
        }

        return modules;
    }

    private static ModuleDefinition CreateSpecializedModuleDefinition(ModuleDefinition module)
    {
        if (module.ModuleType == ModuleType.Storage)
        {
            if (module.Cargo is null)
            {
                throw new InvalidOperationException($"Storage module '{module.Id}' is missing cargo metadata.");
            }

            try
            {
                return new StorageModuleDefinition(module, module.Cargo.Type.ToStorageKind(), module.Cargo.Max);
            }
            catch (ArgumentOutOfRangeException exception)
            {
                throw new InvalidOperationException($"Storage module '{module.Id}' has unsupported cargo type '{module.Cargo.Type}'.", exception);
            }
        }

        if (module.ModuleType == ModuleType.Habitation)
        {
            return new HabitationModuleDefinition(module, module.SerializedWorkforce?.SupportedPopulation ?? 0f);
        }

        if (module.ModuleType == ModuleType.BuildModule)
        {
            return new BuildModuleDefinition(module, module.SerializedWorkforce?.RequiredWorkforce ?? 0f);
        }

        if (module.ModuleType == ModuleType.Production)
        {
            return new ProductionModuleDefinition(module, module.SerializedWorkforce?.RequiredWorkforce ?? 0f);
        }

        return module;
    }
}
