using Theriapolis.Core.Data;
using Theriapolis.Core.Items;
using Theriapolis.Core.Rules.Stats;
using Theriapolis.Core.Util;
// NOTE: deliberately NOT importing Theriapolis.Core.Rules.Character because
// the namespace name collides with the Character class inside it. Fully
// qualify Character; use Allegiance via Rules.Character.Allegiance below.
using Allegiance = Theriapolis.Core.Rules.Character.Allegiance;

namespace Theriapolis.Core.Rules.Combat;

/// <summary>
/// Runtime adapter the resolver works with. Wraps either a
/// <see cref="Character"/> (player + future allies) or an
/// <see cref="NpcTemplateDef"/> (NPCs spawned from chunk lists). Carries
/// the mutable per-encounter state — HP, position, conditions — so the
/// source records aren't touched until the encounter ends and results
/// are written back.
/// </summary>
public sealed class Combatant
{
    public int           Id            { get; }
    public string        Name          { get; }
    public Allegiance    Allegiance    { get; }
    public SizeCategory  Size          { get; }
    public AbilityScores Abilities     { get; }
    public int           ProficiencyBonus { get; }
    public int           ArmorClass    { get; }
    public int           MaxHp         { get; }
    public int           SpeedFt       { get; }
    public int           InitiativeBonus { get; }
    public IReadOnlyList<AttackOption> AttackOptions { get; }

    /// <summary>Source <see cref="Character"/> if built from one (player or ally). Null for NPC-template combatants.</summary>
    public Theriapolis.Core.Rules.Character.Character? SourceCharacter   { get; }
    /// <summary>Source <see cref="NpcTemplateDef"/> if built from one. Null for character combatants.</summary>
    public NpcTemplateDef? SourceTemplate { get; }

    // ── Mutable per-encounter state ───────────────────────────────────────
    public int  CurrentHp { get; set; }
    public Vec2 Position  { get; set; }
    public HashSet<Condition> Conditions { get; } = new();
    /// <summary>Phase 5 M6: present only on player combatants while at 0 HP. Tracks the death-save loop.</summary>
    public DeathSaveTracker? DeathSaves { get; set; }

    // ── Phase 5 M6: per-encounter feature flags ──────────────────────────
    /// <summary>True while Feral Rage is active. Bonus action toggle.</summary>
    public bool RageActive             { get; set; }
    /// <summary>True while Bulwark Sentinel Stance is active. Halves speed; +2 AC.</summary>
    public bool SentinelStanceActive   { get; set; }
    /// <summary>Set when Sneak Attack damage has fired this turn — once-per-turn limit.</summary>
    public bool SneakAttackUsedThisTurn { get; set; }

    // ── Phase 6.5 M1: per-encounter feature state ───────────────────────
    /// <summary>
    /// Pending Vocalization-Dice inspiration die granted by a Muzzle-Speaker.
    /// 0 = none. When non-zero, the next attack/check/save this combatant
    /// rolls adds 1d&lt;value&gt; to the result; the field then resets to 0.
    /// Sides match the Vocalization Dice ladder: 6 / 8 / 10 / 12.
    /// </summary>
    public int InspirationDieSides     { get; set; }

    // ── Phase 6.5 M2: subclass-feature per-encounter state ───────────────
    /// <summary>
    /// Phase 6.5 M2 — Pack-Forged "Packmate's Howl" mark. Set on the target
    /// when a Pack-Forged Fangsworn lands a melee hit; the next attack by
    /// any *ally* of the Pack-Forged on this target gains advantage. The
    /// mark expires when the marker's turn comes around again — tracked
    /// here as the round number the mark was placed; resolver checks
    /// <c>currentRound == HowlMarkRound + 0</c> (current round) or
    /// <c>currentRound == HowlMarkRound + 1</c> (next round, before
    /// marker's turn). Cleared on consume.
    /// </summary>
    public int? HowlMarkRound          { get; set; }
    /// <summary>The Pack-Forged combatant id that placed the howl mark.</summary>
    public int? HowlMarkBy             { get; set; }

    /// <summary>
    /// Phase 6.5 M2 — Blood Memory "Predatory Surge" trigger. Set when this
    /// raging Feral kills a creature with a melee attack; consumed by the
    /// HUD on the next bonus-action prompt (free extra melee attack).
    /// </summary>
    public bool PredatorySurgePending  { get; set; }

    // ── Phase 6.5 M3: Covenant Authority oath mark ───────────────────────
    /// <summary>
    /// Round number when an oath was placed on this combatant (Covenant-
    /// Keeper Covenant's Authority). While the mark is live, the combatant
    /// suffers -2 to attack rolls vs. its marker. Expires 10 rounds after
    /// placement (= 1 minute in d20 round time).
    /// </summary>
    public int? OathMarkRound { get; set; }

    /// <summary>The Covenant-Keeper combatant id who placed the oath mark.</summary>
    public int? OathMarkBy    { get; set; }

    // ── Phase 7 M0: subclass per-turn / per-encounter flags ──────────────
    /// <summary>
    /// Phase 7 M0 — Stampede-Heart "Trampling Charge". Set when this turn's
    /// first melee attack adds the +1d8 bludgeoning bonus; prevents the
    /// bonus from firing twice in one turn. Resets at turn start.
    /// </summary>
    public bool TramplingChargeUsedThisTurn { get; set; }

    /// <summary>
    /// Phase 7 M0 — Ambush-Artist "Opening Strike". Set after the
    /// first melee attack in this encounter consumes the +2d6 bonus; the
    /// bonus only fires once per encounter. Lasts the encounter
    /// (no per-turn reset).
    /// </summary>
    public bool OpeningStrikeUsed { get; set; }

    /// <summary>Reset per-turn flags. Called by Encounter.EndTurn for the incoming actor.</summary>
    public void OnTurnStart()
    {
        SneakAttackUsedThisTurn = false;
        TramplingChargeUsedThisTurn = false;
    }

    /// <summary>True once HP ≤ 0. NPC-template combatants are removed from initiative; character combatants enter death-save mode.</summary>
    public bool IsDown    => CurrentHp <= 0;
    /// <summary>True if either alive (HP &gt; 0) or downed-but-not-dead (rolling death saves).</summary>
    public bool IsAlive   => !IsDown || (DeathSaves is not null && !DeathSaves.Dead);

    private Combatant(
        int id, string name, Allegiance allegiance,
        SizeCategory size, AbilityScores abilities, int profBonus,
        int armorClass, int maxHp, int speedFt, int initiativeBonus,
        IReadOnlyList<AttackOption> attacks,
        Theriapolis.Core.Rules.Character.Character? sourceCharacter, NpcTemplateDef? sourceTemplate,
        Vec2 position)
    {
        Id              = id;
        Name            = name;
        Allegiance      = allegiance;
        Size            = size;
        Abilities       = abilities;
        ProficiencyBonus= profBonus;
        ArmorClass      = armorClass;
        MaxHp           = maxHp;
        SpeedFt         = speedFt;
        InitiativeBonus = initiativeBonus;
        AttackOptions   = attacks;
        SourceCharacter = sourceCharacter;
        SourceTemplate  = sourceTemplate;
        CurrentHp       = maxHp;
        Position        = position;
    }

    /// <summary>
    /// Build a combatant from a <see cref="Character"/>. Pulls AC, HP, and
    /// the primary attack from equipped MainHand (or unarmed strike if none).
    /// </summary>
    public static Combatant FromCharacter(Theriapolis.Core.Rules.Character.Character c, int id, Vec2 position)
    {
        int ac    = DerivedStats.ArmorClass(c);
        int speed = DerivedStats.SpeedFt(c);
        int initBonus = DerivedStats.Initiative(c);
        var attacks = BuildCharacterAttacks(c);
        return new Combatant(
            id, c.Background?.Name is { Length: > 0 } ? $"PC-{id}" : $"PC-{id}",
            c.SourceCharacterAllegiance(), c.Size, c.Abilities, c.ProficiencyBonus,
            ac, c.MaxHp, speed, initBonus, attacks,
            sourceCharacter: c, sourceTemplate: null, position: position);
    }

    /// <summary>
    /// Build a combatant from a <see cref="Character"/> with an explicit
    /// display name (typically the player's chosen name).
    /// </summary>
    public static Combatant FromCharacter(Theriapolis.Core.Rules.Character.Character c, int id, string name, Vec2 position, Allegiance allegiance)
    {
        int ac    = DerivedStats.ArmorClass(c);
        int speed = DerivedStats.SpeedFt(c);
        int initBonus = DerivedStats.Initiative(c);
        var attacks = BuildCharacterAttacks(c);
        return new Combatant(
            id, name, allegiance, c.Size, c.Abilities, c.ProficiencyBonus,
            ac, c.MaxHp, speed, initBonus, attacks,
            sourceCharacter: c, sourceTemplate: null, position: position);
    }

    /// <summary>
    /// Build a combatant from an NPC template. AC and HP come straight from
    /// the template; attacks are mapped 1:1 from <see cref="NpcTemplateDef.Attacks"/>.
    /// </summary>
    public static Combatant FromNpcTemplate(NpcTemplateDef def, int id, Vec2 position)
    {
        var size = SizeExtensions.FromJson(def.Size);
        var abilities = new AbilityScores(
            Score(def.AbilityScores, "STR", 10),
            Score(def.AbilityScores, "DEX", 10),
            Score(def.AbilityScores, "CON", 10),
            Score(def.AbilityScores, "INT", 10),
            Score(def.AbilityScores, "WIS", 10),
            Score(def.AbilityScores, "CHA", 10));
        // NPC profs default to +2 (CR ≤ 4 baseline).
        const int npcProf = 2;
        int initBonus = AbilityScores.Mod(abilities.DEX);
        var attacks = new List<AttackOption>(def.Attacks.Length);
        foreach (var atk in def.Attacks) attacks.Add(BuildNpcAttack(atk));
        // 5 ft. = 1 tactical tile; convert NPC speed_ft to tiles.
        int speedFt = def.SpeedFt;
        var allegiance = Theriapolis.Core.Rules.Character.AllegianceExtensions.FromJson(def.DefaultAllegiance);
        return new Combatant(
            id, def.Name, allegiance, size, abilities, npcProf,
            armorClass: def.Ac, maxHp: def.Hp, speedFt: speedFt, initiativeBonus: initBonus,
            attacks: attacks,
            sourceCharacter: null, sourceTemplate: def, position: position);
    }

    /// <summary>Distance to another combatant in tactical tiles, edge-to-edge Chebyshev.</summary>
    public int DistanceTo(Combatant other) => ReachAndCover.EdgeToEdgeChebyshev(this, other);

    private static int Score(IReadOnlyDictionary<string, int> dict, string key, int fallback)
        => dict.TryGetValue(key, out int v) ? v : fallback;

    /// <summary>Builds the attack option list for a character: equipped weapon if any, else an unarmed strike.</summary>
    private static List<AttackOption> BuildCharacterAttacks(Theriapolis.Core.Rules.Character.Character c)
    {
        var list = new List<AttackOption>();
        var main = c.Inventory.GetEquipped(EquipSlot.MainHand);
        if (main is not null && string.Equals(main.Def.Kind, "weapon", System.StringComparison.OrdinalIgnoreCase))
        {
            list.Add(BuildWeaponAttack(c, main.Def));
        }
        else
        {
            list.Add(BuildUnarmedStrike(c));
        }
        return list;
    }

    private static AttackOption BuildWeaponAttack(Theriapolis.Core.Rules.Character.Character c, ItemDef weapon)
    {
        // Finesse weapons use the higher of STR/DEX; ranged weapons use DEX.
        bool isFinesse = HasProperty(weapon, "finesse");
        bool isRanged  = weapon.RangeShortTiles > 0 || HasProperty(weapon, "ammunition") || HasProperty(weapon, "thrown");
        AbilityId abil = isRanged
            ? AbilityId.DEX
            : (isFinesse
                ? (c.Abilities.ModFor(AbilityId.STR) >= c.Abilities.ModFor(AbilityId.DEX)
                    ? AbilityId.STR : AbilityId.DEX)
                : AbilityId.STR);
        int abilMod = c.Abilities.ModFor(abil);
        // Proficiency: assume the character is proficient with all weapons their class lists.
        // For Phase 5 M4 we apply proficiency unconditionally (every combat-touching class
        // is proficient with their starting weapon). Wrong-proficiency disadvantage lands in M6.
        int toHit = c.ProficiencyBonus + abilMod;

        var damage = DamageRoll.Parse(
            string.IsNullOrEmpty(weapon.Damage) ? "1d4" : weapon.Damage,
            string.IsNullOrEmpty(weapon.DamageType)
                ? DamageType.Bludgeoning
                : DamageTypeExtensions.FromJson(weapon.DamageType));
        damage = damage with { FlatMod = damage.FlatMod + abilMod };

        int reach = weapon.ReachTiles > 0 ? weapon.ReachTiles : c.Size.DefaultReachTiles();

        return new AttackOption
        {
            Name             = weapon.Name,
            ToHitBonus       = toHit,
            Damage           = damage,
            ReachTiles       = isRanged ? 0 : reach,
            RangeShortTiles  = isRanged ? (weapon.RangeShortTiles > 0 ? weapon.RangeShortTiles : 6) : 0,
            RangeLongTiles   = isRanged ? (weapon.RangeLongTiles  > 0 ? weapon.RangeLongTiles  : 24) : 0,
            CritOnNatural    = 20,
        };
    }

    private static AttackOption BuildUnarmedStrike(Theriapolis.Core.Rules.Character.Character c)
    {
        int strMod = c.Abilities.ModFor(AbilityId.STR);
        int toHit  = c.ProficiencyBonus + strMod;
        return new AttackOption
        {
            Name        = "Unarmed Strike",
            ToHitBonus  = toHit,
            Damage      = new DamageRoll(0, 0, System.Math.Max(1, 1 + strMod), DamageType.Bludgeoning),
            ReachTiles  = c.Size.DefaultReachTiles(),
            CritOnNatural = 20,
        };
    }

    private static AttackOption BuildNpcAttack(NpcAttack atk)
    {
        var dmg = DamageRoll.Parse(
            string.IsNullOrEmpty(atk.Damage) ? "1d4" : atk.Damage,
            string.IsNullOrEmpty(atk.DamageType)
                ? DamageType.Bludgeoning
                : DamageTypeExtensions.FromJson(atk.DamageType));
        return new AttackOption
        {
            Name            = atk.Name,
            ToHitBonus      = atk.ToHit,
            Damage          = dmg,
            ReachTiles      = atk.ReachTiles > 0 ? atk.ReachTiles : 1,
            RangeShortTiles = atk.RangeShortTiles,
            RangeLongTiles  = atk.RangeLongTiles,
            CritOnNatural   = 20,
        };
    }

    private static bool HasProperty(ItemDef def, string prop)
    {
        foreach (var p in def.Properties)
            if (string.Equals(p, prop, System.StringComparison.OrdinalIgnoreCase))
                return true;
        return false;
    }
}

/// <summary>Convenience extension so callers needn't know whether a Character has Allegiance attached.</summary>
internal static class CharacterCombatExtensions
{
    public static Allegiance SourceCharacterAllegiance(this Theriapolis.Core.Rules.Character.Character _)
        => Allegiance.Player;
}
