using Theriapolis.Core.Rules.Stats;

namespace Theriapolis.Core.Rules.Combat;

/// <summary>
/// Pure d20-adjacent rule evaluator. Static — no per-encounter state lives
/// here; everything flows through <see cref="Encounter"/> (dice + log) and
/// the supplied <see cref="Combatant"/> instances (HP + conditions).
///
/// Phase 5 M4 ships AttemptAttack, MakeSave, ApplyDamage, ApplyCondition.
/// Class-feature combat effects (Sneak Attack damage, Rage damage bonus,
/// fighting-style modifiers, etc.) are layered on at M6 by inspecting the
/// attacker's <see cref="Character"/> features in <see cref="AttemptAttack"/>.
/// </summary>
public static class Resolver
{
    /// <summary>
    /// Roll an attack from <paramref name="attacker"/> against
    /// <paramref name="target"/>. Logs the outcome on
    /// <paramref name="enc"/>'s log. Mutates target HP if the attack hits.
    /// </summary>
    public static AttackResult AttemptAttack(
        Encounter enc,
        Combatant attacker,
        Combatant target,
        AttackOption attack,
        SituationFlags situation = SituationFlags.None)
    {
        // Range/long-range disadvantage decoration: if the attack is ranged
        // and the target is past short range, OR the calling code is firing
        // a ranged attack into melee, fold those in.
        if (attack.IsRanged && ReachAndCover.IsLongRange(attacker, target, attack))
            situation |= SituationFlags.LongRange;

        // Phase 6.5 M2 — Pack-Forged "Packmate's Howl" consumption: if the
        // target is howl-marked by an ally of this attacker, force advantage
        // on this attack roll.
        if (FeatureProcessor.ConsumeHowlAdvantage(enc, attacker, target))
            situation |= SituationFlags.Advantage;

        // Phase 6.5 M3 — Frightened attackers roll at disadvantage.
        if (attacker.Conditions.Contains(Condition.Frightened))
            situation |= SituationFlags.Disadvantage;

        var (kept, other) = enc.RollD20WithMode(situation);
        // Phase 5 M6: stack Sentinel Stance and other per-combatant AC bonuses.
        // Phase 6.5 M2 — pass the encounter so passive subclass AC features
        // (Herd-Wall Interlock Shields, Lone Fang Isolation Bonus) can read
        // positional state.
        int totalAc = target.ArmorClass + situation.CoverAcBonus()
                    + FeatureProcessor.ApplyAcBonus(target, enc);
        // Phase 6.5 M1: consume an inspiration die (Vocalization Dice) on
        // attack rolls. The bonus applies to the d20 total *before* compare;
        // crits/fumbles still trigger off the natural d20.
        int inspirationBonus = FeatureProcessor.ConsumeInspirationDie(enc, attacker);
        // Phase 6.5 M2 — subclass to-hit bonuses (Lone Fang Isolation Bonus).
        int subclassToHit = FeatureProcessor.ApplyToHitBonus(attacker, enc);
        // Phase 6.5 M3 — Covenant's Authority oath mark: -2 attack vs. marker.
        int oathPenalty   = FeatureProcessor.OathAttackPenalty(enc, attacker, target);
        int attackTotal = kept + attack.ToHitBonus + inspirationBonus + subclassToHit + oathPenalty;

        bool natural1  = kept == 1;
        bool natural20 = kept >= attack.CritOnNatural;
        bool isCrit    = natural20;
        bool hit       = !natural1 && (natural20 || attackTotal >= totalAc);

        int damage = 0;
        if (hit)
        {
            // Damage roll wraps the per-die source so Great Weapon style
            // can re-roll 1s/2s on damage dice without changing the resolver
            // contract. The per-die delegate consumes RNG via the encounter.
            damage = attack.Damage.Roll(
                sides => FeatureProcessor.GreatWeaponReroll(enc, attacker, attack, enc.RollDie(sides), sides),
                isCrit);
            // Per-feature damage bonuses (Duelist, Rage, Sneak Attack).
            damage += FeatureProcessor.ApplyDamageBonus(enc, attacker, target, attack, isCrit);
            // Resistance halves damage (Rage vs phys).
            if (FeatureProcessor.IsResisted(target, attack.Damage.DamageType))
                damage = damage / 2;
            ApplyDamage(target, damage);

            // Phase 6.5 M2 — subclass on-hit triggers.
            // Pack-Forged: melee hit marks the target so allies' next attack
            // gets advantage.
            if (!attack.IsRanged)
                FeatureProcessor.OnPackForgedHit(enc, attacker, target, attack);
            // Blood Memory: melee kill while raging triggers Predatory Surge.
            if (target.IsDown && !attack.IsRanged && attacker.RageActive)
                FeatureProcessor.OnBloodMemoryKill(enc, attacker, attack);

            // Phase 7 M0 — Antler-Guard Retaliatory Strike. Returns 1d8+CON
            // to the attacker when the target is an Antler-Guard in Sentinel
            // Stance hit by a melee attack. Calls ApplyDamage on the attacker
            // directly; the encounter log carries the structured note.
            if (!attack.IsRanged)
                FeatureProcessor.OnAntlerGuardHit(enc, attacker, target, attack);
        }

        var result = new AttackResult
        {
            AttackerId    = attacker.Id,
            TargetId      = target.Id,
            AttackName    = attack.Name,
            D20Roll       = kept,
            D20Other      = other == -1 ? null : other,
            ToHitBonus    = attack.ToHitBonus,
            AttackTotal   = attackTotal,
            TargetAc      = totalAc,
            Hit           = hit,
            Crit          = isCrit && hit,
            DamageRolled  = damage,
            TargetHpAfter = target.CurrentHp,
            Situation     = situation,
        };

        enc.AppendLog(CombatLogEntry.Kind.Attack, result.FormatLog(attacker.Name, target.Name));

        if (target.IsDown)
            enc.AppendLog(CombatLogEntry.Kind.Death, $"{target.Name} falls.");

        return result;
    }

    /// <summary>
    /// Roll a saving throw for <paramref name="target"/> against
    /// <paramref name="dc"/>. Bonus = ability mod + (proficient ? prof : 0).
    /// </summary>
    public static SaveResult MakeSave(
        Encounter enc,
        Combatant target,
        SaveId save,
        int dc,
        bool isProficient = false,
        SituationFlags situation = SituationFlags.None)
    {
        var (kept, _) = enc.RollD20WithMode(situation);
        int bonus = AbilityScores.Mod(target.Abilities.Get(save.Ability()))
                  + (isProficient ? target.ProficiencyBonus : 0);
        int total = kept + bonus;
        bool succ = total >= dc;

        var result = new SaveResult
        {
            TargetId  = target.Id,
            Save      = save,
            D20Roll   = kept,
            SaveBonus = bonus,
            SaveTotal = total,
            Dc        = dc,
            Succeeded = succ,
        };
        enc.AppendLog(CombatLogEntry.Kind.Save,
            $"{target.Name} {save} save: {total} vs DC {dc} → {(succ ? "succeeds" : "fails")}");
        return result;
    }

    /// <summary>
    /// Subtract <paramref name="damage"/> from <paramref name="target"/>'s
    /// HP, clamped to 0. Does not log (callers like AttemptAttack handle
    /// the structured log entry; this is the raw mutation).
    ///
    /// Phase 5 M6: when a player character drops to 0, install a
    /// <see cref="DeathSaveTracker"/> on the combatant; combat HUD reads
    /// this and rolls a save at the start of the player's turn until the
    /// loop resolves (stabilised, revived, or dead).
    /// </summary>
    public static void ApplyDamage(Combatant target, int damage)
    {
        if (damage <= 0) return;
        target.CurrentHp = System.Math.Max(0, target.CurrentHp - damage);
        if (target.CurrentHp == 0 && target.SourceCharacter is not null)
        {
            target.Conditions.Add(Condition.Unconscious);
            target.DeathSaves ??= new DeathSaveTracker();
        }
    }

    /// <summary>Heal up to MaxHp. Negative amounts are no-ops (use ApplyDamage).</summary>
    public static void Heal(Combatant target, int amount)
    {
        if (amount <= 0) return;
        target.CurrentHp = System.Math.Min(target.MaxHp, target.CurrentHp + amount);
        // If the heal lifts a downed character above 0, the unconscious
        // condition lifts automatically and the death-save loop resets.
        if (target.CurrentHp > 0)
        {
            target.Conditions.Remove(Condition.Unconscious);
            target.DeathSaves?.Reset();
        }
    }

    /// <summary>Apply a condition to a target. Logs the change.</summary>
    public static void ApplyCondition(Encounter enc, Combatant target, Condition condition)
    {
        if (target.Conditions.Add(condition))
            enc.AppendLog(CombatLogEntry.Kind.ConditionApplied,
                $"{target.Name} is now {condition}.");
    }

    /// <summary>Remove a condition from a target. Logs if it was present.</summary>
    public static void RemoveCondition(Encounter enc, Combatant target, Condition condition)
    {
        if (target.Conditions.Remove(condition))
            enc.AppendLog(CombatLogEntry.Kind.ConditionEnded,
                $"{target.Name} is no longer {condition}.");
    }
}
