using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using PkmnLib.Dataloader.Models;
using PkmnLib.Static;
using PkmnLib.Static.Libraries;
using PkmnLib.Static.Species;
using PkmnLib.Static.Utils;

namespace PkmnLib.Dataloader;

public static class SpeciesDataLoader
{
    private static Dictionary<string, SerializedSpecies> LoadSpeciesData(Stream stream)
    {
        var obj = JsonSerializer.Deserialize<JsonObject>(stream);
        if (obj == null)
            throw new InvalidDataException("Species data is empty.");
        var jsonConfig = new JsonSerializerOptions()
        {
            PropertyNameCaseInsensitive = true,
        };
        return obj.Where(x => x.Key != "$schema")
            .ToDictionary(x => x.Key, x => x.Value.Deserialize<SerializedSpecies>(jsonConfig));
    }
    
    public static SpeciesLibrary LoadSpecies(Stream[] streams, IReadOnlyTypeLibrary typeLibrary)
    {
        var library = new SpeciesLibrary();
        var objects = streams.SelectMany(LoadSpeciesData);
        if (objects == null)
            throw new InvalidDataException("Species data is empty.");
        var species = objects.Select(x => DeserializeSpecies(x.Value, typeLibrary));
        foreach (var s in species)
            library.Add(s);
        return library;
    }
    
    public static SpeciesLibrary LoadSpecies(Stream stream, IReadOnlyTypeLibrary typeLibrary)
    {
        var library = new SpeciesLibrary();
        var objects = LoadSpeciesData(stream);
        if (objects == null)
            throw new InvalidDataException("Species data is empty.");
        var species = objects.Select(x => DeserializeSpecies(x.Value, typeLibrary));
        foreach (var s in species)
            library.Add(s);
        return library;
    }

    public static Func<SerializedSpecies, ushort, StringKey, float, StringKey, byte, byte, IReadOnlyDictionary<StringKey, IForm>,
        IEnumerable<StringKey>, IReadOnlyList<IEvolution>, IEnumerable<StringKey>, SpeciesImpl> SpeciesConstructor =
        (_, id, name, genderRate, growthRate, captureRate, baseHappiness, forms, flags, evolutionData,
            eggGroups) =>
    {
        return 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.");

        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());
    }

    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)
    {
        return new ImmutableStatisticSet<ushort>(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."),
        };
    }
}