using PkmnLib.Dynamic.AI.Explicit;
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
using PkmnLib.Plugin.Gen7.Scripts.Side;
using PkmnLib.Plugin.Gen7.Scripts.Status;
using PkmnLib.Static.Moves;

namespace PkmnLib.Plugin.Gen7.AI;

public static class AISwitchFunctions
{
    internal static void RegisterAISwitchFunctions(ExplicitAIHandlers handlers)
    {
        handlers.ShouldSwitchFunctions.Add("perish_song", PerishSong);
        handlers.ShouldSwitchFunctions.Add("significant_end_of_turn_damage", SignificantEndOfTurnDamage);
        handlers.ShouldSwitchFunctions.Add("high_damage_from_foe", HighDamageFromFoe);
        handlers.ShouldSwitchFunctions.Add("cure_status_problem_by_switching_out", CureStatusProblemBySwitchingOut);
    }

    /// <summary>
    /// Switch out if the Perish Song effect is about to cause the Pokémon to faint.
    /// </summary>
    private static bool PerishSong(IExplicitAI ai, IPokemon pokemon, IBattle battle, IReadOnlyList<IPokemon> reserves)
    {
        if (!pokemon.Volatile.TryGet<PerishSongEffect>(out var effect))
            return false;
        return effect.Turns <= 1;
    }

    private static readonly StringKey PoisonHealAbilityName = "poison_heal";

    /// <summary>
    /// Switch out if the Pokémon is expected to take significant end-of-turn damage.
    /// </summary>
    private static bool SignificantEndOfTurnDamage(IExplicitAI ai, IPokemon pokemon, IBattle battle,
        IReadOnlyList<IPokemon> reserves)
    {
        var eorDamage = 0;
        pokemon.RunScriptHook<IAIInfoScriptExpectedEndOfTurnDamage>(x =>
            x.ExpectedEndOfTurnDamage(pokemon, ref eorDamage));
        if (eorDamage >= pokemon.CurrentHealth / 2 || eorDamage >= pokemon.MaxHealth / 4)
            return true;
        if (!ai.TrainerHighSkill || eorDamage <= 0)
            return false;

        if (pokemon.Volatile.Contains<LeechSeedEffect>() && ai.Random.GetBool())
            return true;
        if (pokemon.Volatile.Contains<NightmareEffect>())
            return true;
        if (pokemon.Volatile.Contains<GhostCurseEffect>())
            return true;
        var statusScript = pokemon.StatusScript.Script;
        if (statusScript is BadlyPoisoned { Turns: > 0 } badlyPoisoned &&
            pokemon.ActiveAbility?.Name != PoisonHealAbilityName)
        {
            var poisonDamage = pokemon.MaxHealth / 8;
            var nextToxicDamage = pokemon.MaxHealth * badlyPoisoned.GetPoisonMultiplier();
            if ((pokemon.CurrentHealth <= nextToxicDamage && pokemon.CurrentHealth > poisonDamage) ||
                nextToxicDamage > poisonDamage * 2)
            {
                return true;
            }
        }
        return false;
    }

    private static bool HighDamageFromFoe(IExplicitAI ai, IPokemon pokemon, IBattle battle,
        IReadOnlyList<IPokemon> reserves)
    {
        if (!ai.TrainerHighSkill)
            return false;
        if (pokemon.CurrentHealth >= pokemon.MaxHealth / 2)
            return false;
        var bigThreat = false;
        var opponents = battle.Sides.Where(x => x != pokemon.BattleData?.BattleSide)
            .SelectMany(x => x.Pokemon.WhereNotNull());
        foreach (var opponent in opponents)
        {
            if (Math.Abs(opponent.Level - pokemon.Level) > 5)
                continue;
            var lastMoveUsed = opponent.BattleData?.LastMoveChoice;
            if (lastMoveUsed is null)
                continue;
            var moveData = lastMoveUsed.ChosenMove.MoveData;
            if (moveData.Category == MoveCategory.Status)
                continue;
            var effectiveness = pokemon.Library.StaticLibrary.Types.GetEffectiveness(moveData.MoveType, pokemon.Types);
            if (effectiveness <= 1 || moveData.BasePower < 70)
                continue;
            var switchChange = moveData.BasePower > 90 ? 50 : 25;
            bigThreat = ai.Random.GetInt(100) < switchChange;
        }
        return bigThreat;
    }

    private static readonly StringKey ImmunityAbilityName = "immunity";
    private static readonly StringKey InsomniaAbilityName = "insomnia";
    private static readonly StringKey LimberAbilityName = "limber";
    private static readonly StringKey MagmaArmorAbilityName = "magma_armor";
    private static readonly StringKey VitalSpiritAbilityName = "vital_spirit";
    private static readonly StringKey WaterBubbleAbilityName = "water_bubble";
    private static readonly StringKey WaterVeilAbilityName = "water_veil";
    private static readonly StringKey NaturalCureAbilityName = "natural_cure";
    private static readonly StringKey RegeneratorAbilityName = "regenerator";

    private static readonly Dictionary<StringKey, List<StringKey>> StatusCureAbilities = new()
    {
        {
            ImmunityAbilityName,
            [ScriptUtils.ResolveName<Poisoned>(), ScriptUtils.ResolveName<BadlyPoisoned>()]
        },
        { InsomniaAbilityName, [ScriptUtils.ResolveName<Sleep>()] },
        { LimberAbilityName, [ScriptUtils.ResolveName<Paralyzed>()] },
        { MagmaArmorAbilityName, [ScriptUtils.ResolveName<Frozen>()] },
        { VitalSpiritAbilityName, [ScriptUtils.ResolveName<Sleep>()] },
        { WaterBubbleAbilityName, [ScriptUtils.ResolveName<Burned>()] },
        { WaterVeilAbilityName, [ScriptUtils.ResolveName<Burned>()] },
    };

    /// <summary>
    /// Switch out to cure a status problem or heal HP with abilities like Natural Cure or Regenerator.
    /// </summary>
    private static bool CureStatusProblemBySwitchingOut(IExplicitAI ai, IPokemon pokemon, IBattle battle,
        IReadOnlyList<IPokemon> reserves)
    {
        if (pokemon.ActiveAbility == null)
            return false;

        // Don't try to cure a status problem/heal a bit of HP if entry hazards will
        // KO the battler if it switches back in
        var entryHazardDamage = ExplicitAI.CalculateEntryHazardDamage(pokemon, pokemon.BattleData!.BattleSide);
        if (entryHazardDamage >= pokemon.CurrentHealth)
            return false;
        if (pokemon.StatusScript.Script is null)
            return false;

        var abilityName = pokemon.ActiveAbility.Name;
        var statusName = pokemon.StatusScript.Script.Name;

        // Check abilities that cure specific status conditions
        var canCureStatus = false;
        if (abilityName == NaturalCureAbilityName)
        {
            canCureStatus = true;
        }
        else if (StatusCureAbilities.TryGetValue(abilityName, out var statusList))
        {
            canCureStatus = statusList.Any(status => status == statusName);
        }

        if (canCureStatus)
        {
            if (AIHelperFunctions.WantsStatusProblem(pokemon, statusName))
                return false;

            // Don't bother if the status will cure itself soon
            if (pokemon.StatusScript.Script is Sleep { Turns: 1 })
                return false;

            if (entryHazardDamage >= pokemon.MaxHealth / 4)
                return false;

            // Don't bother curing a poisoning if Toxic Spikes will just re-poison
            if (pokemon.StatusScript.Script is Poisoned or BadlyPoisoned &&
                !reserves.Any(p => p.Types.Any(t => t.Name == "poison")))
            {
                if (pokemon.BattleData!.BattleSide.VolatileScripts.TryGet<ToxicSpikesEffect>(out _))
                {
                    return false;
                }
            }

            // Not worth curing status problems that still allow actions if at high HP
            var isImmobilizing = pokemon.StatusScript.Script is Sleep or Frozen;
            if (pokemon.CurrentHealth >= pokemon.MaxHealth / 2 && !isImmobilizing)
                return false;

            if (ai.Random.GetInt(100) < 70)
                return true;
        }
        else if (abilityName == RegeneratorAbilityName)
        {
            // Not worth healing if battler would lose more HP from switching back in later
            if (entryHazardDamage >= pokemon.MaxHealth / 3)
                return false;

            // Not worth healing HP if already at high HP
            if (pokemon.CurrentHealth >= pokemon.MaxHealth / 2)
                return false;

            // Don't bother if a foe is at low HP and could be knocked out instead
            var hasDamagingMove = pokemon.Moves.Any(m => m?.MoveData.Category != MoveCategory.Status);
            if (hasDamagingMove)
            {
                var opponents = battle.Sides.Where(x => x != pokemon.BattleData?.BattleSide)
                    .SelectMany(x => x.Pokemon.WhereNotNull());
                var weakFoe = opponents.Any(opponent => opponent.CurrentHealth < opponent.MaxHealth / 3);
                if (weakFoe)
                    return false;
            }

            if (ai.Random.GetInt(100) < 70)
                return true;
        }

        return false;
    }
}