using PkmnLib.Dynamic.BattleFlow;
using PkmnLib.Dynamic.Libraries;
using PkmnLib.Dynamic.Models;
using PkmnLib.Dynamic.Models.Choices;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Static.Moves;
using PkmnLib.Static.Utils;

namespace PkmnLib.Dynamic.AI.Explicit;

/// <summary>
/// An explicit AI that has explicitly written logic for each Pokémon and move.
/// </summary>
/// <remarks>
/// This is heavily based on the AI used in <a href="https://github.com/Maruno17/pokemon-essentials">Pokémon Essentials</a>
/// </remarks>
public interface IExplicitAI
{
    /// <summary>
    /// Whether the trainer is of high skill level.
    /// </summary>
    bool TrainerHighSkill { get; }

    /// <summary>
    /// Whether the trainer is of medium skill level.
    /// </summary>
    bool TrainerMediumSkill { get; }

    /// <summary>
    /// The random number generator used by the AI.
    /// </summary>
    IRandom Random { get; }
}

/// <inheritdoc cref="IExplicitAI"/>
public partial class ExplicitAI : PokemonAI, IExplicitAI
{
    /// <summary>
    /// The score assigned to a move that is predicted to fail.
    /// </summary>
    public const int MoveFailScore = 20;

    /// <summary>
    /// The score assigned to a move that is considered useless.
    /// </summary>
    public const int MoveUselessScore = 60;

    /// <summary>
    /// The base score assigned to a move before modifiers.
    /// </summary>
    public const int MoveBaseScore = 100;

    private readonly float _trainerSkill = 100;
    private readonly SkillFlags _skillFlags = new();

    private float MoveScoreThreshold => (float)(0.6f + 0.35f * Math.Sqrt(Math.Min(_trainerSkill, 100) / 100f));

    private readonly IReadOnlyExplicitAIHandlers _handlers;

    private readonly IRandom _random = new RandomImpl();

    /// <summary>
    /// Flags that control the skill of the AI.
    /// </summary>
    public class SkillFlags
    {
        /// <summary>
        /// Whether the AI can predict if a move will fail.
        /// </summary>
        public bool CanPredictMoveFailure { get; init; } = true;

        /// <summary>
        /// Whether the AI will score moves to determine their usefulness.
        /// </summary>
        public bool ScoreMoves { get; init; } = true;

        /// <summary>
        /// Whether the AI will consider switching out Pokémon.
        /// </summary>
        public bool ConsiderSwitching { get; init; } = true;

        /// <summary>
        /// Whether the AI will reserve the last Pokémon in its party (ace).
        /// </summary>
        public bool ReserveLastPokemon { get; init; } = false;

        /// <summary>
        /// Whether the AI will use Pokémon in the order they are in the party, or choose freely.
        /// </summary>
        public bool UsePokemonInOrder { get; init; } = false;
    }

    /// <inheritdoc cref="ExplicitAI"/>
    /// <param name="library">The library for Pokémon data.</param>
    public ExplicitAI(IDynamicLibrary library) : base("explicit")
    {
        _handlers = library.ExplicitAIHandlers;
    }

    /// <inheritdoc cref="ExplicitAI(IDynamicLibrary)"/>
    /// <param name="library">The library for Pokémon data.</param>
    /// <param name="trainerSkill">The skill level of the trainer (0-100).</param>
    /// <param name="skillFlags">The skill flags that control the AI's behavior.</param>
    public ExplicitAI(IDynamicLibrary library, float trainerSkill, SkillFlags skillFlags) : base("explicit")
    {
        _handlers = library.ExplicitAIHandlers;
        _trainerSkill = trainerSkill;
        _skillFlags = skillFlags;
    }

    /// <inheritdoc />
    public bool TrainerHighSkill => _trainerSkill >= 45;

    /// <inheritdoc />
    public bool TrainerMediumSkill => _trainerSkill >= 32;

    /// <inheritdoc />
    public IRandom Random => _random;

    /// <inheritdoc />
    public override ITurnChoice GetChoice(IBattle battle, IPokemon pokemon)
    {
        if (battle.HasForcedTurn(pokemon, out var choice))
            return choice;
        if (TryChooseToSwitchOut(battle, pokemon, false, out var turnChoice))
        {
            AILogging.LogInformation($"{pokemon} is switching out.");
            return turnChoice;
        }

        var moveChoices = GetMoveScores(pokemon, battle);
        if (moveChoices.Count == 0)
        {
            var opponentSide = (byte)(pokemon.BattleData!.SideIndex == 0 ? 1 : 0);
            return battle.Library.MiscLibrary.ReplacementChoice(pokemon, opponentSide, pokemon.BattleData.Position);
        }
        var maxScore = moveChoices.Max(x => x.score);
        if (TrainerHighSkill && CanSwitch(pokemon))
        {
            var badMoves = false;
            if (maxScore <= MoveUselessScore)
            {
                badMoves = CanAttack(pokemon);
                if (!badMoves && _random.GetInt(100) < 25)
                    badMoves = true;
            }
            else if (maxScore < MoveBaseScore * MoveScoreThreshold && pokemon.BattleData?.TurnsOnField > 2 &&
                     _random.GetInt(100) < 80)
            {
                badMoves = true;
            }
            if (badMoves)
            {
                AILogging.LogInformation($"{pokemon} has no good moves, considering switching.");
                if (TryChooseToSwitchOut(battle, pokemon, badMoves, out var switchChoice))
                {
                    return switchChoice;
                }
            }
        }

        var threshold = (float)Math.Floor(maxScore * MoveScoreThreshold);
        var considerChoices = moveChoices.Select(x => (x, Math.Max(x.score - threshold, 0))).ToArray();
        var totalScore = considerChoices.Sum(x => x.Item2);
        if (totalScore == 0)
        {
            var opponentSide = (byte)(pokemon.BattleData!.SideIndex == 0 ? 1 : 0);
            return battle.Library.MiscLibrary.ReplacementChoice(pokemon, opponentSide, pokemon.BattleData.Position);
        }
        var initialRandomValue = _random.GetFloat(0, totalScore);
        var randomValue = initialRandomValue;
        for (var i = 0; i < considerChoices.Length; i++)
        {
            randomValue -= considerChoices[i].Item2;
            if (randomValue >= 0)
                continue;

            var (index, _, targetIndex) = considerChoices[i].x;
            var learnedMove = pokemon.Moves[index];
            var opponentSide = (byte)(pokemon.BattleData!.SideIndex == 0 ? 1 : 0);
            if (targetIndex == -1)
                targetIndex = pokemon.BattleData.Position;
            return new MoveChoice(pokemon, learnedMove!, opponentSide, (byte)targetIndex);
        }
        throw new InvalidOperationException("No valid move choice found. This should not happen.");
    }

    private List<(int index, int score, int targetIndex)> GetMoveScores(IPokemon user, IBattle battle)
    {
        var choices = new List<(int index, int score, int targetIndex)>();
        foreach (var (learnedMove, index) in user.Moves.Select((x, i) => (x, i)).Where(x => x.x != null))
        {
            var moveChoice = new MoveChoice(user, learnedMove!, 0, 0);
            if (!battle.CanUse(moveChoice))
            {
                if (learnedMove!.CurrentPp == 0 && learnedMove.MaxPp > 0)
                {
                    AILogging.LogInformation($"{user} cannot use {learnedMove} because it has no PP left.");
                }
                else
                {
                    AILogging.LogInformation($"{user} cannot use {learnedMove} because it is not a valid choice.");
                }
                continue;
            }
            var moveData = learnedMove!.MoveData;
            var moveName = moveData.Name;
            moveChoice.RunScriptHook<IScriptChangeMove>(x => x.ChangeMove(moveChoice, ref moveName));
            if (moveName != learnedMove.MoveData.Name)
            {
                AILogging.LogInformation($"{user} changed {learnedMove} to {moveName}.");
                if (!battle.Library.StaticLibrary.Moves.TryGet(moveName, out moveData))
                    throw new InvalidOperationException($"Move {moveName} not found in the move library.");
                var secondaryEffect = moveData.SecondaryEffect;
                if (secondaryEffect != null)
                {
                    if (moveChoice.User.Library.ScriptResolver.TryResolve(ScriptCategory.Move, secondaryEffect.Name,
                            secondaryEffect.Parameters, out var script))
                    {
                        moveChoice.Script.Set(script);
                        script.OnAddedToParent(moveChoice);
                    }
                    else
                    {
                        moveChoice.Script.Clear();
                    }
                }
                else
                {
                    moveChoice.Script.Clear();
                }
            }
            var aiMove = new AIMoveState(user, moveData);
            if (_skillFlags.CanPredictMoveFailure && PredictMoveFailure(user, battle, aiMove))
            {
                AILogging.LogInformation($"{user} is considering {aiMove.Move.Name} but it will fail.");
                AddMoveToChoices(index, MoveFailScore);
            }

            var target = moveData.Target;
            if (target is MoveTarget.All or MoveTarget.AllAlly or MoveTarget.AllOpponent or MoveTarget.SelfUse)
            {
                var score = GetMoveScore(user, aiMove, battle);
                AddMoveToChoices(index, score);
            }
            else if (target is MoveTarget.AdjacentOpponent or MoveTarget.Any or MoveTarget.Adjacent
                     or MoveTarget.AdjacentAllySelf)
            {
                // TODO: get redirected target
                foreach (var pokemon in battle.Sides.SelectMany(x => x.Pokemon).WhereNotNull())
                {
                    var battleData = pokemon.BattleData;
                    if (battleData == null)
                        continue;
                    if (!TargetResolver.IsValidTarget(battleData.SideIndex, battleData.Position, target, user))
                        continue;
                    if (target.TargetsFoe() && battleData.SideIndex == user.BattleData?.SideIndex)
                    {
                        continue;
                    }
                    var score = GetMoveScoreAgainstTarget(user, aiMove, pokemon, battle);
                    AddMoveToChoices(index, score, battleData.Position);
                }
            }
            else
            {
                var targets = new List<IPokemon>();
                foreach (var pokemon in battle.Sides.SelectMany(x => x.Pokemon).WhereNotNull())
                {
                    var battleData = pokemon.BattleData;
                    if (battleData == null)
                        continue;
                    if (!TargetResolver.IsValidTarget(battleData.SideIndex, battleData.Position, target, user))
                        continue;
                    if (target.TargetsFoe() && battleData.SideIndex == user.BattleData?.SideIndex)
                    {
                        continue;
                    }
                    targets.Add(pokemon);
                }
                var score = GetMoveScore(user, aiMove, battle, targets);
                AddMoveToChoices(index, score);
            }
        }
        return choices;

        void AddMoveToChoices(int index, int score, int targetIndex = -1)
        {
            choices.Add((index, score, targetIndex));
            // If the user is a wild Pokémon, doubly prefer a random move.
            if (battle.IsWildBattle && user.PersonalityValue % user.Moves.Count == index)
            {
                choices.Add((index, score, targetIndex));
            }
        }
    }

    private bool PredictMoveFailure(IPokemon user, IBattle battle, AIMoveState aiMove)
    {
        if (user.HasStatus("sleep"))
        {
            // User is asleep, and will not wake up, and the move is not usable while asleep.
            if (user.GetStatusTurnsLeft != 0 && !aiMove.Move.HasFlag("usable_while_asleep"))
            {
                AILogging.LogInformation($"{user} is asleep and cannot use {aiMove.Move.Name}.");
                return true;
            }
        }
        // The move is only usable while asleep, but the user is not asleep
        else if (aiMove.Move.HasFlag("usable_while_asleep"))
        {
            return true;
        }

        // Primal weather
        if (battle.WeatherName == "primordial_sea" && aiMove.Move.MoveType.Name == "fire")
            return true;
        if (battle.WeatherName == "desolate_lands" && aiMove.Move.MoveType.Name == "water")
            return true;

        // Check if the move will fail based on the handlers
        return aiMove.Move.SecondaryEffect != null && _handlers.MoveWillFail(this, aiMove.Move.SecondaryEffect.Name,
            new MoveOption(aiMove, battle, null));
    }

    private static readonly StringKey PsychicTerrainName = new("psychic_terrain");
    private static readonly StringKey DazzlingName = new("dazzling");
    private static readonly StringKey QueenlyMajestyName = new("queenly_majesty");
    private static readonly StringKey PranksterName = new("prankster");
    private static readonly StringKey DarkName = new("dark");
    private static readonly StringKey GroundName = new("ground");
    private static readonly StringKey PowderName = new("powder");
    private static readonly StringKey SubstituteName = new("substitute");
    private static readonly StringKey InfiltratorName = new("infiltrator");

    private bool PredictMoveFailureAgainstTarget(IPokemon user, AIMoveState aiMove, IPokemon target, IBattle battle)
    {
        if (aiMove.Move.SecondaryEffect != null && _handlers.MoveWillFailAgainstTarget(this,
                aiMove.Move.SecondaryEffect.Name, new MoveOption(aiMove, battle, target)))
            return true;
        if (aiMove.Move.Priority > 0)
        {
            if (target.BattleData?.SideIndex != user.BattleData?.SideIndex)
            {
                // Psychic Terrain makes all priority moves fail if the target is affected
                if (battle.TerrainName == PsychicTerrainName && !target.IsFloating)
                {
                    return true;
                }
                // Dazzling and Queenly Majesty prevent priority moves from being used against the Pokémon with those abilities
                if (target.BattleData?.BattleSide.Pokemon.WhereNotNull().Any(x =>
                        x.ActiveAbility?.Name == DazzlingName || x.ActiveAbility?.Name == QueenlyMajestyName) == true)
                {
                    return true;
                }
            }
        }
        // TODO: Check immunity because of ability

        var moveType = aiMove.Move.MoveType;
        var typeEffectiveness = battle.Library.StaticLibrary.Types.GetEffectiveness(moveType, target.Types);
        if (aiMove.Move.Category != MoveCategory.Status && typeEffectiveness == 0)
            return true;
        if (user.ActiveAbility?.Name == PranksterName && aiMove.Move.Category == MoveCategory.Status &&
            target.Types.Any(x => x.Name == DarkName) && target.BattleData?.SideIndex != user.BattleData?.SideIndex)
            return true;
        if (aiMove.Move.Category != MoveCategory.Status && moveType.Name == GroundName && target.IsFloating)
            return true;
        if (aiMove.Move.HasFlag(PowderName) && !AffectedByPowder(target))
            return true;
        if (target.Volatile.Contains(SubstituteName) && aiMove.Move.Category == MoveCategory.Status &&
            user.ActiveAbility?.Name != InfiltratorName)
            return true;

        return false;
    }

    private int GetMoveScore(IPokemon user, AIMoveState aiMove, IBattle battle, IReadOnlyList<IPokemon>? targets = null)
    {
        var score = MoveBaseScore;
        if (targets != null)
        {
            score = 0;
            var affectedTargets = 0;
            foreach (var target in targets)
            {
                var targetScore = GetMoveScoreAgainstTarget(user, aiMove, target, battle);
                if (targetScore < 0)
                    continue;
                score += targetScore;
                affectedTargets++;
            }
            if (affectedTargets == 0 && _skillFlags.CanPredictMoveFailure)
            {
                return MoveFailScore;
            }
            if (affectedTargets > 0)
                score = (int)(score / (float)affectedTargets);
        }
        if (_skillFlags.ScoreMoves)
        {
            if (aiMove.Move.SecondaryEffect != null)
            {
                _handlers.ApplyMoveEffectScore(this, aiMove.Move.SecondaryEffect.Name,
                    new MoveOption(aiMove, battle, null), ref score);
                _handlers.ApplyGenerateMoveScoreModifiers(this, new MoveOption(aiMove, battle, null), ref score);
            }
        }
        if (score < 0)
            score = 0;
        return score;
    }

    private int GetMoveScoreAgainstTarget(IPokemon user, AIMoveState aiMove, IPokemon target, IBattle battle)
    {
        if (_skillFlags.CanPredictMoveFailure && PredictMoveFailureAgainstTarget(user, aiMove, target, battle))
        {
            AILogging.LogInformation($"{user} is considering {aiMove.Move.Name} against {target} but it will fail.");
            return -1;
        }
        var score = MoveBaseScore;
        if (_skillFlags.ScoreMoves && aiMove.Move.SecondaryEffect != null)
        {
            var estimatedDamage = AIHelpers.CalculateDamageEstimation(aiMove.Move, user, target, battle.Library);
            var moveOption = new MoveOption(aiMove, battle, target, estimatedDamage);
            _handlers.ApplyMoveEffectAgainstTargetScore(this, aiMove.Move.SecondaryEffect.Name, moveOption, ref score);
            _handlers.ApplyGenerateMoveAgainstTargetScoreModifiers(this, moveOption, ref score);
        }

        if (aiMove.Move.Target.TargetsFoe() && target.BattleData?.SideIndex == user.BattleData?.SideIndex &&
            target.BattleData?.Position != user.BattleData?.Position)
        {
            if (score == MoveUselessScore)
                return -1;
            score = (int)(1.85f * MoveBaseScore - score);
        }

        return score;
    }

    private static readonly StringKey GrassName = new("grass");
    private static readonly StringKey OvercoatName = new("overcoat");
    private static readonly StringKey SafetyGogglesName = new("safety_goggles");

    private static bool AffectedByPowder(IPokemon pokemon)
    {
        if (pokemon.Types.Any(x => x.Name == GrassName))
            return false;
        if (pokemon.ActiveAbility?.Name == OvercoatName)
            return false;
        if (pokemon.HasHeldItem(SafetyGogglesName))
            return false;
        return true;
    }

    private static bool CanAttack(IPokemon pokemon)
    {
        if (pokemon.Volatile.Contains("requires_recharge"))
            return false;
        if (pokemon.HasStatus("frozen") || pokemon.HasStatus("sleep"))
            return false;
        if (pokemon.ActiveAbility?.Name == "truant" && pokemon.Volatile.Contains("truant_effect"))
            return false;
        if (pokemon.Volatile.Contains("flinch_effect"))
            return false;
        return true;
    }
}