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

namespace PkmnLib.Plugin.Gen7.AI;

public static class AIHelperFunctions
{
    public static int GetScoreForTargetStatRaise(int score, AIMoveState move, IPokemon target,
        StatisticSet<sbyte> statChanges, bool fixedChange = false, bool ignoreContrary = false)
    {
        var wholeEffect = move.Move.Category != MoveCategory.Status;

        var desireMult = 1;
        if (move.User.BattleData?.SideIndex != target.BattleData?.SideIndex)
            desireMult = -1;

        if (!ignoreContrary && !fixedChange && target.ActiveAbility?.Name == "contrary")
        {
            if (desireMult > 0 && wholeEffect)
            {
                return ExplicitAI.MoveUselessScore;
            }
            return GetScoreForTargetStatDrop(score, move, target, statChanges, fixedChange, true);
        }

        var addEffect = move.GetScoreChangeForAdditionalEffect(target);
        if (addEffect == -999)
        {
            return score;
        }
        var expectedEndOfTurnDamage = 0;
        target.RunScriptHook<IAIInfoScriptExpectedEndOfTurnDamage>(x =>
            x.ExpectedEndOfTurnDamage(target, ref expectedEndOfTurnDamage));
        // If the target is expected to faint from the end of turn damage, we don't want to
        // apply the score for the stat raise, as it will not be able to use it.
        if (expectedEndOfTurnDamage >= target.CurrentHealth)
            return wholeEffect ? ExplicitAI.MoveUselessScore : score;

        if (!move.User.HasMoveWithEffect("power_trip"))
        {
            var foeIsAware = target.BattleData?.BattleSide.Pokemon.Any(x => x?.ActiveAbility?.Name == "unaware") !=
                             true;
            if (!foeIsAware)
            {
                return wholeEffect ? ExplicitAI.MoveUselessScore : score;
            }
        }
        var realStatChanges = new StatBoostStatisticSet();
        foreach (var (stat, i) in statChanges)
        {
            var increment = i;
            if (!IsStatRaiseWorthwhile(target, stat, increment, fixedChange))
            {
                continue;
            }
            if (!fixedChange && target.ActiveAbility?.Name == "simple")
            {
                increment *= 2;
            }
            increment = (sbyte)Math.Max(increment,
                StatBoostStatisticSet.MaxStatBoost - target.StatBoost.GetStatistic(stat));
            realStatChanges.SetStatistic(stat, increment);
        }
        if (realStatChanges.IsEmpty)
        {
            return wholeEffect ? ExplicitAI.MoveUselessScore : score;
        }

        score += addEffect;
        score = GetTargetStatRaiseScoreGeneric(score, target, realStatChanges, move, desireMult);

        foreach (var realStatChange in realStatChanges.Where(x => x.value > 0))
        {
            GetTargetStatRaiseScoreOne(ref score, target, realStatChange.statistic, realStatChange.value, move,
                desireMult);
        }
        return score;
    }

    public static int GetScoreForTargetStatDrop(int score, AIMoveState move, IPokemon target,
        StatisticSet<sbyte> statChanges, bool fixedChange = false, bool ignoreContrary = false)
    {
        var wholeEffect = move.Move.Category != MoveCategory.Status;

        var desireMult = -1;
        if (move.User.BattleData?.SideIndex == target.BattleData?.SideIndex)
            desireMult = 1;
        if (!ignoreContrary && !fixedChange && target.ActiveAbility?.Name == "contrary")
        {
            if (desireMult > 0 && wholeEffect)
            {
                return ExplicitAI.MoveUselessScore;
            }
            return GetScoreForTargetStatRaise(score, move, target, statChanges, fixedChange, true);
        }
        var addEffect = move.GetScoreChangeForAdditionalEffect(target);
        if (addEffect == -999)
        {
            return score;
        }
        var expectedEndOfTurnDamage = 0;
        target.RunScriptHook<IAIInfoScriptExpectedEndOfTurnDamage>(x =>
            x.ExpectedEndOfTurnDamage(target, ref expectedEndOfTurnDamage));
        // If the target is expected to faint from the end of turn damage, the stat drop is useless
        if (expectedEndOfTurnDamage >= target.CurrentHealth)
            return wholeEffect ? ExplicitAI.MoveUselessScore : score;

        var foeIsAware = false;
        if (target.BattleData?.BattleSide.Pokemon.All(x => x?.ActiveAbility?.Name != "unaware") == true)
        {
            foeIsAware = true;
        }
        if (!foeIsAware)
        {
            return wholeEffect ? ExplicitAI.MoveUselessScore : score;
        }
        var realStatChanges = new StatBoostStatisticSet();
        foreach (var (stat, i) in statChanges)
        {
            var decrement = i;
            if (!IsStatDropWorthwhile(target, stat, decrement))
            {
                continue;
            }
            if (!fixedChange && target.ActiveAbility?.Name == "simple")
            {
                decrement *= 2;
            }
            decrement = (sbyte)Math.Min(decrement,
                StatBoostStatisticSet.MinStatBoost - target.StatBoost.GetStatistic(stat));
            realStatChanges.SetStatistic(stat, (sbyte)-decrement);
        }
        if (realStatChanges.IsEmpty)
            return wholeEffect ? ExplicitAI.MoveUselessScore : score;
        score += addEffect;
        score = GetTargetStatDropScoreGeneric(score, target, realStatChanges, move, desireMult);
        foreach (var realStatChange in realStatChanges.Where(x => x.value > 0))
        {
            GetTargetStatDropScoreOne(ref score, target, realStatChange.statistic, realStatChange.value, move,
                desireMult);
        }
        return score;
    }

    /// <summary>
    /// Checks if a stat raise is worthwhile for the given Pokémon and stat.
    /// </summary>
    private static bool IsStatRaiseWorthwhile(IPokemon pokemon, Statistic stat, sbyte amount, bool fixedChange = false)
    {
        if (!fixedChange && pokemon.StatBoost.GetStatistic(stat) == StatBoostStatisticSet.MaxStatBoost)
            return false;
        if (!pokemon.HasMoveWithEffect("power_trip", "baton_pass"))
            return true;

        switch (stat)
        {
            case Statistic.Attack:
            {
                if (!pokemon.Moves.WhereNotNull().Any(x => x.MoveData.Category == MoveCategory.Physical &&
                                                           x.MoveData.SecondaryEffect?.Name != "foul_play"))
                {
                    return false;
                }
                break;
            }
            case Statistic.Defense:
            {
                var opponentSide = pokemon.BattleData!.Battle.Sides.First(x => x != pokemon.BattleData.BattleSide);
                return opponentSide.Pokemon.WhereNotNull().Any(x => x.Moves.WhereNotNull().Any(y =>
                    y.MoveData.Category == MoveCategory.Physical || y.MoveData.SecondaryEffect?.Name == "psyshock"));
            }
            case Statistic.SpecialAttack:
            {
                if (pokemon.Moves.WhereNotNull().All(x => x.MoveData.Category != MoveCategory.Special))
                {
                    return false;
                }
                break;
            }
            case Statistic.SpecialDefense:
            {
                var opponentSide = pokemon.BattleData!.Battle.Sides.First(x => x != pokemon.BattleData.BattleSide);
                return opponentSide.Pokemon.WhereNotNull().Any(x => x.Moves.WhereNotNull().Any(y =>
                    y.MoveData.Category == MoveCategory.Special && y.MoveData.SecondaryEffect?.Name != "psyshock"));
            }
            case Statistic.Speed:
            {
                if (!pokemon.HasMoveWithEffect("electro_ball", "power_trip"))
                {
                    var targetSpeed = pokemon.BoostedStats.Speed;
                    var opponentSide = pokemon.BattleData!.Battle.Sides.First(x => x != pokemon.BattleData.BattleSide);
                    var meaningful = opponentSide.Pokemon.WhereNotNull().Select(opponent => opponent.BoostedStats.Speed)
                        .Any(foeSpeed => targetSpeed < foeSpeed && targetSpeed * 2.5 > foeSpeed);
                    if (!meaningful)
                        return false;
                }
                break;
            }
            case Statistic.Accuracy:
            {
                var minAccuracy = pokemon.Moves.WhereNotNull().Min(x => x.MoveData.Accuracy);
                if (minAccuracy >= 90 && pokemon.StatBoost.Accuracy >= 0)
                {
                    var meaningful = false;
                    var opponentSide = pokemon.BattleData!.Battle.Sides.First(x => x != pokemon.BattleData.BattleSide);
                    if (opponentSide.Pokemon.WhereNotNull().Any(x => x.StatBoost.Evasion > 0))
                    {
                        meaningful = true;
                    }
                    if (!meaningful)
                        return false;
                }
                break;
            }
        }
        return true;
    }

    private static bool IsStatDropWorthwhile(IPokemon pokemon, Statistic stat, sbyte amount)
    {
        if (amount == 0)
            return false;
        switch (stat)
        {
            case Statistic.Attack:
            {
                return pokemon.Moves.WhereNotNull().Any(x => x.MoveData.Category == MoveCategory.Physical &&
                                                             x.MoveData.SecondaryEffect?.Name != FoulPlayAbilityName);
            }
            case Statistic.Defense:
            {
                var opponentSide = pokemon.BattleData!.Battle.Sides.First(x => x != pokemon.BattleData.BattleSide);
                return opponentSide.Pokemon.WhereNotNull().Any(x => x.Moves.WhereNotNull().Any(y =>
                    y.MoveData.Category == MoveCategory.Physical || y.MoveData.SecondaryEffect?.Name == "psyshock"));
            }
            case Statistic.SpecialAttack:
            {
                return pokemon.Moves.WhereNotNull().Any(x => x.MoveData.Category == MoveCategory.Special);
            }
            case Statistic.SpecialDefense:
            {
                var opponentSide = pokemon.BattleData!.Battle.Sides.First(x => x != pokemon.BattleData.BattleSide);
                return opponentSide.Pokemon.WhereNotNull().Any(x => x.Moves.WhereNotNull().Any(y =>
                    y.MoveData.Category == MoveCategory.Special && y.MoveData.SecondaryEffect?.Name != "psyshock"));
            }
            case Statistic.Speed:
            {
                if (!pokemon.HasMoveWithEffect("electro_ball"))
                {
                    var targetSpeed = pokemon.BoostedStats.Speed;
                    var opponentSide = pokemon.BattleData!.Battle.Sides.First(x => x != pokemon.BattleData.BattleSide);
                    return opponentSide.Pokemon.WhereNotNull().Select(opponent => opponent.BoostedStats.Speed)
                        .Any(foeSpeed => targetSpeed > foeSpeed && targetSpeed < foeSpeed * 2.5);
                }
                return true;
            }
            case Statistic.Accuracy:
            {
                var meaningful = pokemon.Moves.WhereNotNull().Any(x => x.MoveData.Accuracy != 255);
                return meaningful;
            }
        }
        return true;
    }

    private static readonly StringKey FoulPlayAbilityName = "foul_play";

    private static void GetTargetStatRaiseScoreOne(ref int score, IPokemon target, Statistic stat, sbyte increment,
        AIMoveState move, float desireMult = 1)
    {
        var oldStage = target.StatBoost.GetStatistic(stat);
        var newStage = (sbyte)(oldStage + increment);
        var incMult = Gen7BattleStatCalculator.GetStatBoostModifier(Math.Min(newStage, (sbyte)6)) /
                      Gen7BattleStatCalculator.GetStatBoostModifier(oldStage);
        var actualIncrement = incMult;
        incMult -= 1;
        incMult *= desireMult;
        var opponentSide = target.BattleData!.Battle.Sides.First(x => x != target.BattleData.BattleSide);

        switch (stat)
        {
            case Statistic.Attack:
            {
                if (oldStage >= 2 && increment == 1)
                    score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult));
                else
                {
                    var hasSpecialMoves = target.Moves.WhereNotNull()
                        .Any(x => x.MoveData.Category == MoveCategory.Special);
                    var inc = hasSpecialMoves ? 8 : 12;
                    score += (int)(inc * incMult);
                }
                break;
            }
            case Statistic.Defense:
            {
                if (oldStage >= 2 && increment == 1)
                    score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult));
                else
                    score += (int)(10 * incMult);
                break;
            }
            case Statistic.SpecialAttack:
            {
                if (oldStage >= 2 && increment == 1)
                    score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult));
                else
                {
                    var hasPhysicalMoves = target.Moves.WhereNotNull().Any(x =>
                        x.MoveData.Category == MoveCategory.Physical &&
                        x.MoveData.SecondaryEffect?.Name != FoulPlayAbilityName);
                    var inc = hasPhysicalMoves ? 8 : 12;
                    score += (int)(inc * incMult);
                }
                break;
            }
            case Statistic.SpecialDefense:
            {
                if (oldStage >= 2 && increment == 1)
                    score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult));
                else
                    score += (int)(10 * incMult);
                break;
            }
            case Statistic.Speed:
            {
                var targetSpeed = target.BoostedStats.Speed;
                foreach (var opponent in opponentSide.Pokemon.WhereNotNull())
                {
                    var foeSpeed = opponent.BoostedStats.Speed;
                    if (foeSpeed <= targetSpeed)
                        continue;
                    if (foeSpeed > targetSpeed * 2.5)
                        continue;
                    if (targetSpeed * actualIncrement > foeSpeed)
                        score += (int)(15 * incMult);
                    else
                        score += (int)(8 * incMult);
                }
                if (target.HasMoveWithEffect("electro_ball", "power_trip"))
                {
                    score += (int)(5 * incMult);
                }
                if (opponentSide.Pokemon.WhereNotNull().Any(x => x.HasMoveWithEffect("gyro_ball")))
                {
                    score -= (int)(5 * incMult);
                }
                if (target.ActiveAbility?.Name == "speed_boost")
                {
                    score -= (int)(15 * (target.Opposes(move.User) ? 1 : desireMult));
                }
                break;
            }
            case Statistic.Accuracy:
            {
                if (oldStage >= 2 && increment == 1)
                {
                    score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult));
                }
                else
                {
                    var minAccuracy = target.Moves.WhereNotNull().Min(x => x.MoveData.Accuracy);
                    var previousMinAccuracy =
                        minAccuracy * Gen7BattleStatCalculator.GetAccuracyEvasionStatModifier(0, oldStage);
                    if (previousMinAccuracy < 90)
                    {
                        score += (int)(10 * incMult);
                    }
                }
                break;
            }
            case Statistic.Evasion:
            {
                foreach (var opponent in opponentSide.Pokemon.WhereNotNull())
                {
                    var endOfTurnDamage = 0;
                    opponent.RunScriptHook<IAIInfoScriptExpectedEndOfTurnDamage>(x =>
                        x.ExpectedEndOfTurnDamage(opponent, ref endOfTurnDamage));
                    if (endOfTurnDamage > 0)
                        score += (int)(5 * incMult);
                }
                if (oldStage >= 2 && increment == 1)
                {
                    score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult));
                }
                else
                {
                    score += (int)(10 * incMult);
                }
                break;
            }
        }
        if (target.HasMoveWithEffect("power_trip"))
        {
            score += (int)(5 * increment * desireMult);
        }
        if (opponentSide.Pokemon.WhereNotNull().Any(x => x.HasMoveWithEffect("punishment")))
        {
            score -= (int)(5 * increment * desireMult);
        }
    }

    private static void GetTargetStatDropScoreOne(ref int score, IPokemon target, Statistic stat, sbyte decrement,
        AIMoveState move, float desireMult = 1)
    {
        var oldStage = target.StatBoost.GetStatistic(stat);
        var newStage = (sbyte)(oldStage - decrement);
        var decMult = Gen7BattleStatCalculator.GetStatBoostModifier(oldStage) /
                      Gen7BattleStatCalculator.GetStatBoostModifier(Math.Max(newStage, (sbyte)-6));
        decMult -= 1;
        decMult *= desireMult;
        var opponentSide = target.BattleData!.Battle.Sides.First(x => x != target.BattleData.BattleSide);

        switch (stat)
        {
            case Statistic.Attack:
            {
                if (oldStage <= -2 && decrement == 1)
                    score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult));
                else
                {
                    var hasSpecialMoves = target.Moves.WhereNotNull()
                        .Any(x => x.MoveData.Category == MoveCategory.Special);
                    var dec = hasSpecialMoves ? 8 : 12;
                    score += (int)(dec * decMult);
                }
                break;
            }
            case Statistic.Defense:
            {
                if (oldStage <= -2 && decrement == 1)
                    score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult));
                else
                    score += (int)(10 * decMult);
                break;
            }
            case Statistic.SpecialAttack:
            {
                if (oldStage <= -2 && decrement == 1)
                    score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult));
                else
                {
                    var hasPhysicalMoves = target.Moves.WhereNotNull().Any(x =>
                        x.MoveData.Category == MoveCategory.Physical &&
                        x.MoveData.SecondaryEffect?.Name != FoulPlayAbilityName);
                    var dec = hasPhysicalMoves ? 8 : 12;
                    score += (int)(dec * decMult);
                }
                break;
            }
            case Statistic.SpecialDefense:
            {
                if (oldStage <= -2 && decrement == 1)
                    score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult));
                else
                    score += (int)(10 * decMult);
                break;
            }
            case Statistic.Speed:
            {
                var targetSpeed = target.BoostedStats.Speed;
                foreach (var opponent in opponentSide.Pokemon.WhereNotNull())
                {
                    var foeSpeed = opponent.BoostedStats.Speed;
                    if (targetSpeed < foeSpeed)
                        continue;
                    if (targetSpeed > foeSpeed * 2.5)
                        continue;
                    if (targetSpeed < foeSpeed * 2 / (decrement + 2))
                        score += (int)(15 * decMult);
                    else
                        score += (int)(8 * decMult);
                    break;
                }
                if (opponentSide.Pokemon.WhereNotNull().Any(x => x.HasMoveWithEffect("electro_ball")))
                {
                    score += (int)(5 * decMult);
                }
                if (target.ActiveAbility?.Name == "speed_boost")
                {
                    score -= (int)(15 * (target.Opposes(move.User) ? 1 : desireMult));
                }
                break;
            }
            case Statistic.Accuracy:
            {
                if (oldStage <= -2 && decrement == 1)
                    score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult));
                else
                    score += (int)(10 * decMult);
                break;
            }
            case Statistic.Evasion:
            {
                if (oldStage <= -2 && decrement == 1)
                    score -= (int)(10 * (target.Opposes(move.User) ? 1 : desireMult));
                else
                    score += (int)(10 * decMult);
                break;
            }
        }
        if (target.HasMoveWithEffect("power_trip"))
        {
            score += (int)(5 * decrement * desireMult);
        }
        if (opponentSide.Pokemon.WhereNotNull().Any(x => x.HasMoveWithEffect("punishment")))
        {
            score -= (int)(5 * decrement * desireMult);
        }
    }

    /// <summary>
    /// Calculates the score for the generic concept of raising a target's stats.
    /// </summary>
    private static int GetTargetStatRaiseScoreGeneric(int score, IPokemon target, StatisticSet<sbyte> statChanges,
        AIMoveState move, float desireMult = 1)
    {
        var totalIncrement = statChanges.Sum(x => x.value);
        var turns = target.BattleData!.Battle.CurrentTurnNumber - target.BattleData!.SwitchInTurn;
        if (turns < 2 && move.Move.Category == MoveCategory.Status)
            score += (int)(totalIncrement * desireMult * 5);

        score +=
            (int)(totalIncrement * desireMult * ((100 * (target.CurrentHealth / (float)target.MaxHealth) - 50) / 8));
        return score;
    }

    private static int GetTargetStatDropScoreGeneric(int score, IPokemon target, StatisticSet<sbyte> statChanges,
        AIMoveState move, float desireMult = 1)
    {
        var totalDecrement = statChanges.Sum(x => x.value);
        var turns = target.BattleData!.Battle.CurrentTurnNumber - target.BattleData!.SwitchInTurn;
        if (turns < 2 && move.Move.Category == MoveCategory.Status)
            score += (int)(totalDecrement * desireMult * 5);

        score +=
            (int)(totalDecrement * desireMult * ((100 * (target.CurrentHealth / (float)target.MaxHealth) - 50) / 8));
        return score;
    }

    private static int GetScoreChangeForAdditionalEffect(this AIMoveState move, IPokemon? target)
    {
        if (move.Move.SecondaryEffect is null)
            return 0;
        if (move.User.ActiveAbility?.Name == "sheer_force")
            return -999;
        if (target is not null && target.BattleData?.Position != move.User.BattleData?.Position &&
            target.ActiveAbility?.Name == "shield_dust")
            return -999;

        if ((move.Move.SecondaryEffect.Chance < 100 && move.User.ActiveAbility?.Name == "serene_grace") ||
            move.User.BattleData?.BattleSide.VolatileScripts.Contains<RainbowEffect>() == true)
        {
            return 5;
        }
        return 0;
    }

    private static bool HasMoveWithEffect(this IPokemon pokemon, params StringKey[] effect)
    {
        return pokemon.Moves.WhereNotNull().Any(move => move.MoveData.SecondaryEffect?.Name is not null &&
                                                        effect.Contains(move.MoveData.SecondaryEffect.Name));
    }

    private static bool Opposes(this IPokemon pokemon, IPokemon target) =>
        pokemon.BattleData?.BattleSide != target.BattleData?.BattleSide;

    public static bool WantsStatusProblem(IPokemon pokemon, StringKey? status)
    {
        if (status is null)
            return true;
        if (pokemon.ActiveAbility != null)
        {
            if (pokemon.ActiveAbility.Name == "guts" && status != ScriptUtils.ResolveName<Sleep>() &&
                status != ScriptUtils.ResolveName<Frozen>() &&
                IsStatRaiseWorthwhile(pokemon, Statistic.Attack, 1, true))
            {
                return true;
            }
            if (pokemon.ActiveAbility.Name == "marvel_scale" &&
                IsStatRaiseWorthwhile(pokemon, Statistic.Defense, 1, true))
            {
                return true;
            }
            if (pokemon.ActiveAbility.Name == "quick_feet" && status != ScriptUtils.ResolveName<Sleep>() &&
                status != ScriptUtils.ResolveName<Frozen>() && IsStatRaiseWorthwhile(pokemon, Statistic.Speed, 1, true))
            {
                return true;
            }
            if (pokemon.ActiveAbility.Name == "flare_boost" && status == ScriptUtils.ResolveName<Burned>() &&
                IsStatRaiseWorthwhile(pokemon, Statistic.SpecialAttack, 1, true))
            {
                return true;
            }
            if (pokemon.ActiveAbility.Name == "toxic_boost" &&
                (status == ScriptUtils.ResolveName<Poisoned>() || status == ScriptUtils.ResolveName<BadlyPoisoned>()) &&
                IsStatRaiseWorthwhile(pokemon, Statistic.Attack, 1, true))
            {
                return true;
            }
            if (pokemon.ActiveAbility.Name == "poison_heal" && status == ScriptUtils.ResolveName<Poisoned>())
            {
                return true;
            }
            if (pokemon.ActiveAbility.Name == "magic_guard")
            {
                if (status != ScriptUtils.ResolveName<Poisoned>() &&
                    status != ScriptUtils.ResolveName<BadlyPoisoned>() && status != ScriptUtils.ResolveName<Burned>())
                    return false;
                if (IsStatRaiseWorthwhile(pokemon, Statistic.Attack, 1, true))
                {
                    return true;
                }
            }
        }
        return false;
    }
}