using System.Collections.Immutable;
using System.Text.Json;
using System.Text.Json.Nodes;
using PkmnLib.Dynamic.Libraries.DataLoaders.Models;
using PkmnLib.Static;
using PkmnLib.Static.Libraries;
using PkmnLib.Static.Species;
using PkmnLib.Static.Utils;

namespace PkmnLib.Dynamic.Libraries.DataLoaders;

/// <summary>
/// Loads species data from a JSON file.
/// </summary>
public static class SpeciesDataLoader
{
    /// <summary>
    /// Deserializes a typical JSON object containing species data into a dictionary of species.
    /// </summary>
    public static Dictionary<string, SerializedSpecies> LoadSpeciesData(Stream stream)
    {
        var obj = JsonSerializer.Deserialize<JsonObject>(stream, JsonOptions.DefaultOptions);
        if (obj == null)
            throw new InvalidDataException("Species data is empty.");
        return obj.Where(x => x.Key != "$schema").ToDictionary(x => x.Key,
            x => x.Value.Deserialize<SerializedSpecies>(JsonOptions.DefaultOptions))!;
    }

    /// <summary>
    /// Loads the species library from a JSON file.
    /// </summary>
    public static SpeciesLibrary LoadSpecies(Stream stream, IReadOnlyTypeLibrary typeLibrary,
        Action<Dictionary<string, SerializedSpecies>>? action = null)
    {
        var library = new SpeciesLibrary();
        var objects = LoadSpeciesData(stream);
        if (objects == null)
            throw new InvalidDataException("Species data is empty.");
        action?.Invoke(objects);
        var species = objects.Select(x => DeserializeSpecies(x.Value, typeLibrary));
        foreach (var s in species)
            library.Add(s);
        return library;
    }

    /// <summary>
    /// Factory delegate for creating species.
    /// </summary>
    public delegate SpeciesImpl SpeciesFactoryDelegate(SerializedSpecies serialized, ushort id, StringKey name,
        float genderRate, StringKey growthRate, byte captureRate, byte baseHappiness,
        IReadOnlyDictionary<StringKey, IForm> forms, IEnumerable<StringKey> flags,
        IReadOnlyList<IEvolution> evolutionData, IEnumerable<StringKey> eggGroups);

    /// <summary>
    /// The species constructor. This is used to create species from the JSON data.
    /// </summary>
    [PublicAPI] public static SpeciesFactoryDelegate SpeciesConstructor =
        (_, id, name, genderRate, growthRate, captureRate, baseHappiness, forms, flags, evolutionData, eggGroups) =>
            new SpeciesImpl(id, name, genderRate, growthRate, captureRate, baseHappiness, forms, flags, evolutionData,
                eggGroups);

    private static SpeciesImpl DeserializeSpecies(SerializedSpecies serialized, IReadOnlyTypeLibrary typeLibrary)
    {
        var id = serialized.Id;
        var genderRate = serialized.GenderRatio;
        if (genderRate < -1.0 || genderRate > 100.0)
        {
            throw new InvalidDataException(
                $"Gender rate for species {id} is invalid: {genderRate}. Must be between -1.0 and 100.0.");
        }

        if (serialized.EggCycles < 0)
        {
            throw new InvalidDataException(
                $"Egg cycles for species {id} is invalid: {serialized.EggCycles}. Must be greater than or equal to 0.");
        }

        foreach (var form in serialized.Formes)
        {
            if (!string.IsNullOrEmpty(form.Value.InheritFrom))
            {
                var inheritedForm = serialized.Formes.GetValueOrDefault(form.Value.InheritFrom);
                if (inheritedForm == null)
                {
                    throw new InvalidDataException(
                        $"Form {form.Key} inherits from {form.Value.InheritFrom}, but that form does not exist.");
                }
                form.Value.MakeInheritFrom(inheritedForm);
            }
        }

        var forms = serialized.Formes.ToDictionary(x => (StringKey)x.Key,
            x => DeserializeForm(x.Key, x.Value, typeLibrary));
        var evolutions = serialized.Evolutions.Select(DeserializeEvolution).ToList();

        var species = SpeciesConstructor(serialized, serialized.Id, serialized.Species, genderRate,
            serialized.GrowthRate, serialized.CatchRate, serialized.BaseHappiness, forms,
            serialized.Flags.Select(x => new StringKey(x)), evolutions, serialized.EggGroups.Select(x => (StringKey)x));
        return species;
    }

    private static IForm DeserializeForm(string name, SerializedForm form, IReadOnlyTypeLibrary typeLibrary)
    {
        if (form == null)
            throw new ArgumentException("Form data is null.", nameof(form));
        if (form.Height < 0.0)
        {
            throw new InvalidDataException(
                $"Height for form {name} is invalid: {form.Height}. Must be greater than or equal to 0.0.");
        }
        if (form.Weight < 0.0)
        {
            throw new InvalidDataException(
                $"Weight for form {name} is invalid: {form.Weight}. Must be greater than or equal to 0.0.");
        }
        var types = form.Types.Select(x =>
            typeLibrary.TryGetTypeIdentifier(new StringKey(x), out var t)
                ? t
                : throw new InvalidDataException($"Type {x} for form {name} is invalid.")).ToList();

        return new FormImpl(name, form.Height, form.Weight, form.BaseExp, types, DeserializeStats(form.BaseStats),
            form.Abilities.Select(x => new StringKey(x)).ToList(),
            form.HiddenAbilities.Select(x => new StringKey(x)).ToList(), DeserializeMoves(form.Moves),
            form.Flags.Select(x => new StringKey(x)).ToImmutableHashSet(), form.IsBattleOnly || form.IsMega);
    }

    private static ILearnableMoves DeserializeMoves(SerializedMoves moves)
    {
        var learnableMoves = new LearnableMovesImpl();
        if (moves.LevelMoves != null)
        {
            foreach (var levelMove in moves.LevelMoves)
            {
                learnableMoves.AddLevelMove((byte)levelMove.Level, new StringKey(levelMove.Name));
            }
        }

        if (moves.EggMoves != null)
        {
            foreach (var eggMove in moves.EggMoves)
            {
                learnableMoves.AddEggMove(new StringKey(eggMove));
            }
        }

        return learnableMoves;
    }

    private static ImmutableStatisticSet<ushort> DeserializeStats(SerializedStats stats) =>
        new(stats.Hp, stats.Attack, stats.Defense, stats.SpecialAttack, stats.SpecialDefense, stats.Speed);

    private static IEvolution DeserializeEvolution(SerializedEvolution evolution)
    {
        return evolution.Method.ToLowerInvariant() switch
        {
            "level" => new LevelEvolution
            {
                Level = evolution.Data.GetValue<byte>(),
                ToSpecies = evolution.Species,
            },
            "levelfemale" => new LevelGenderEvolution
            {
                Level = evolution.Data.GetValue<byte>(),
                Gender = Gender.Female,
                ToSpecies = evolution.Species,
            },
            "levelmale" => new LevelGenderEvolution
            {
                Level = evolution.Data.GetValue<byte>(),
                Gender = Gender.Male,
                ToSpecies = evolution.Species,
            },
            "happiness" => new HappinessEvolution
            {
                Happiness = evolution.Data.GetValue<byte>(),
                ToSpecies = evolution.Species,
            },
            "happinessday" => new HappinessDayEvolution
            {
                Happiness = evolution.Data.GetValue<byte>(),
                ToSpecies = evolution.Species,
            },
            "happinessnight" => new HappinessNightEvolution
            {
                Happiness = evolution.Data.GetValue<byte>(),
                ToSpecies = evolution.Species,
            },
            "item" => new ItemUseEvolution
            {
                Item = evolution.Data.GetValue<string>() ?? throw new InvalidDataException("Item is null."),
                ToSpecies = evolution.Species,
            },
            "itemmale" => new ItemGenderEvolution
            {
                Item = evolution.Data.GetValue<string>() ?? throw new InvalidDataException("Item is null."),
                ToSpecies = evolution.Species,
            },
            "itemfemale" => new ItemGenderEvolution
            {
                Item = evolution.Data.GetValue<string>() ?? throw new InvalidDataException("Item is null."),
                ToSpecies = evolution.Species,
            },
            "holditem" => new HoldItemEvolution
            {
                Item = evolution.Data.GetValue<string>() ?? throw new InvalidDataException("Item is null."),
                ToSpecies = evolution.Species,
            },
            "dayholditem" => new DayHoldItemEvolution
            {
                Item = evolution.Data.GetValue<string>() ?? throw new InvalidDataException("Item is null."),
                ToSpecies = evolution.Species,
            },
            "nightholditem" => new NightHoldItemEvolution
            {
                Item = evolution.Data.GetValue<string>() ?? throw new InvalidDataException("Item is null."),
                ToSpecies = evolution.Species,
            },
            "hasmove" => new HasMoveEvolution
            {
                MoveName = evolution.Data.GetValue<string>() ?? throw new InvalidDataException("Move is null."),
                ToSpecies = evolution.Species,
            },
            "trade" => new TradeEvolution
            {
                ToSpecies = evolution.Species,
            },
            "tradespecies" => new TradeSpeciesEvolution
            {
                WithSpecies = evolution.Data.GetValue<string>() ?? throw new InvalidDataException("Species is null."),
                ToSpecies = evolution.Species,
            },
            "tradeitem" => new TradeItemEvolution
            {
                Item = evolution.Data.GetValue<string>() ?? throw new InvalidDataException("Item is null."),
                ToSpecies = evolution.Species,
            },
            "location" => new CustomEvolution
            {
                Name = "location",
                Parameters = new Dictionary<StringKey, object?>
                {
                    ["location"] = evolution.Data.ToString() ?? throw new InvalidDataException("Location is null."),
                },
                ToSpecies = evolution.Species,
            },
            "custom" => new CustomEvolution
            {
                Name = evolution.Data.AsObject()["type"]?.GetValue<string>() ??
                       throw new InvalidDataException("Type is null."),
                Parameters = evolution.Data.AsObject().Where(x => x.Key != "type")
                    .ToDictionary(x => new StringKey(x.Key), x => x.Value!.ToParameter()),
                ToSpecies = evolution.Species,
            },
            _ => throw new InvalidDataException($"Evolution type {evolution.Method} is invalid."),
        };
    }
}