using Theriapolis.Core.Data;
using Theriapolis.Core.Rules.Character;
using Theriapolis.Core.Rules.Combat;
using Theriapolis.Core.Rules.Stats;
using Theriapolis.Core.Util;

namespace Theriapolis.Tools.Commands;

/// <summary>
/// Headless combat scenario runner. Builds two combatants, places them on
/// an empty arena, and runs a simple "close distance, then attack" AI loop
/// until one side falls or the round cap is hit. Prints the full encounter
/// log so test scenarios and balance sweeps can grep the output.
///
/// Usage:
///   dotnet run --project Theriapolis.Tools -- combat-duel \
///     --a brigand_footpad --b wolf --seed 42 [--rounds 20] [--data-dir ./Content/Data]
///
/// Combatant specs:
///   - An NPC template id (e.g. "brigand_footpad", "wolf", "bear_brown")
///   - A character spec "char:CLADE:SPECIES:CLASS:BACKGROUND" (e.g.
///     "char:canidae:wolf:fangsworn:pack_raised") — uses Standard Array
///     stats and the class's starting kit.
/// </summary>
public static class CombatDuel
{
    public static int Run(string[] args)
    {
        ulong  seed     = 42UL;
        string dataDir  = "./Content/Data";
        string specA    = "brigand_footpad";
        string specB    = "wolf";
        int    maxRounds = 20;

        for (int i = 0; i < args.Length; i++)
        {
            switch (args[i])
            {
                case "--seed":     seed      = ulong.Parse(args[++i]); break;
                case "--data-dir": dataDir   = args[++i]; break;
                case "--a":        specA     = args[++i]; break;
                case "--b":        specB     = args[++i]; break;
                case "--rounds":   maxRounds = int.Parse(args[++i]); break;
            }
        }

        var loader  = new ContentLoader(dataDir);
        var content = new ContentResolver(loader);

        Combatant a, b;
        try
        {
            a = BuildCombatant(specA, content, id: 1, position: new Vec2(0, 0),
                               allegiance: Allegiance.Player);
            b = BuildCombatant(specB, content, id: 2, position: new Vec2(6, 0),
                               allegiance: Allegiance.Hostile);
        }
        catch (System.Exception ex)
        {
            System.Console.Error.WriteLine($"Failed to build combatants: {ex.Message}");
            return 1;
        }

        var enc = new Encounter(seed, encounterId: 1, new[] { a, b });

        System.Console.WriteLine($"=== combat-duel: {a.Name} vs {b.Name} ===");
        System.Console.WriteLine($"Seed: 0x{seed:X}  encounterSeed: 0x{enc.EncounterSeed:X}");
        System.Console.WriteLine($"Initial positions: {a.Name} ({a.Position.X},{a.Position.Y}) HP {a.CurrentHp} AC {a.ArmorClass}; " +
                                 $"{b.Name} ({b.Position.X},{b.Position.Y}) HP {b.CurrentHp} AC {b.ArmorClass}");
        System.Console.WriteLine();

        int turnsTaken = 0;
        // Hard safety cap: at most 4 actions per combatant per round.
        int maxTurns = maxRounds * (a.AttackOptions.Count + b.AttackOptions.Count) * 4 + 4;
        while (!enc.IsOver && enc.RoundNumber <= maxRounds && turnsTaken < maxTurns)
        {
            turnsTaken++;
            DriveTurn(enc);
            enc.EndTurn();
        }

        if (enc.RoundNumber > maxRounds && !enc.IsOver)
            System.Console.WriteLine($"--- Round cap reached ({maxRounds} rounds). ---");

        System.Console.WriteLine();
        System.Console.WriteLine("--- Combat log ---");
        foreach (var entry in enc.Log)
            System.Console.WriteLine($"R{entry.Round} T{entry.Turn} [{entry.Type}] {entry.Message}");
        System.Console.WriteLine();

        var aliveA = a.IsAlive && !a.IsDown;
        var aliveB = b.IsAlive && !b.IsDown;
        System.Console.WriteLine($"Final: {a.Name} HP {a.CurrentHp}/{a.MaxHp} {(aliveA ? "alive" : "down")}; " +
                                 $"{b.Name} HP {b.CurrentHp}/{b.MaxHp} {(aliveB ? "alive" : "down")}");
        System.Console.WriteLine($"Total dice rolled: {enc.RollCount}");
        return 0;
    }

    /// <summary>Simple one-attack-per-turn AI: move toward the nearest hostile, attack when in reach.</summary>
    private static void DriveTurn(Encounter enc)
    {
        var actor = enc.CurrentActor;
        if (!actor.IsAlive || actor.IsDown) return;

        var target = FindHostile(enc, actor);
        if (target is null) return;
        var attack = actor.AttackOptions[0];

        // Movement budget — convert ft. to tiles. 5 ft. = 1 tile (d20 standard).
        int tilesAvailable = enc.CurrentTurn.RemainingMovementFt / 5;
        while (!ReachAndCover.IsInReach(actor, target, attack) && tilesAvailable > 0)
        {
            var next = ReachAndCover.StepToward(actor.Position, target.Position);
            if (next.X == actor.Position.X && next.Y == actor.Position.Y) break;
            actor.Position = next;
            enc.AppendLog(CombatLogEntry.Kind.Move,
                $"{actor.Name} moves to ({next.X},{next.Y}).");
            tilesAvailable--;
        }
        // Charge the consumed movement back to the turn budget.
        int consumed = (enc.CurrentTurn.RemainingMovementFt / 5) - tilesAvailable;
        enc.CurrentTurn.ConsumeMovement(consumed * 5);

        if (!ReachAndCover.IsInReach(actor, target, attack)) return;

        Resolver.AttemptAttack(enc, actor, target, attack);
        enc.CurrentTurn.ConsumeAction();
    }

    private static Combatant? FindHostile(Encounter enc, Combatant actor)
    {
        // Hostile = different allegiance side. Player + Allied are friends; Hostile vs Player.
        Combatant? best = null;
        int bestDist = int.MaxValue;
        foreach (var c in enc.Participants)
        {
            if (c.Id == actor.Id) continue;
            if (!c.IsAlive || c.IsDown) continue;
            if (!IsHostileTo(actor.Allegiance, c.Allegiance)) continue;
            int d = ReachAndCover.EdgeToEdgeChebyshev(actor, c);
            if (d < bestDist) { best = c; bestDist = d; }
        }
        return best;
    }

    private static bool IsHostileTo(Allegiance a, Allegiance b)
    {
        bool aIsPlayerSide = a == Allegiance.Player || a == Allegiance.Allied;
        bool bIsPlayerSide = b == Allegiance.Player || b == Allegiance.Allied;
        if (aIsPlayerSide && b == Allegiance.Hostile) return true;
        if (bIsPlayerSide && a == Allegiance.Hostile) return true;
        return false;
    }

    private static Combatant BuildCombatant(string spec, ContentResolver content, int id, Vec2 position, Allegiance allegiance)
    {
        if (spec.StartsWith("char:", System.StringComparison.OrdinalIgnoreCase))
        {
            var parts = spec.Split(':');
            if (parts.Length != 5)
                throw new System.ArgumentException($"Character spec '{spec}' must be char:clade:species:class:background");
            string cladeId   = parts[1];
            string speciesId = parts[2];
            string classId   = parts[3];
            string bgId      = parts[4];
            var character = BuildLevel1Character(content, cladeId, speciesId, classId, bgId);
            string name = character.Species.Name + " " + character.ClassDef.Name;
            return Combatant.FromCharacter(character, id, name, position, allegiance);
        }

        // Otherwise treat as an NPC template id.
        if (!content.Npcs.Templates.Any(t => string.Equals(t.Id, spec, System.StringComparison.OrdinalIgnoreCase)))
            throw new System.ArgumentException($"Unknown NPC template id: '{spec}'");
        var def = content.Npcs.Templates.First(t => string.Equals(t.Id, spec, System.StringComparison.OrdinalIgnoreCase));
        var combatant = Combatant.FromNpcTemplate(def, id, position);
        // Allow caller to override allegiance (e.g. force a brigand to fight a wolf).
        // A new Combatant doesn't expose Allegiance setter; build a fresh one with the override.
        if (combatant.Allegiance != allegiance)
        {
            // Cheap rebuild: clone the template's combatant with overridden allegiance.
            combatant = CloneWithAllegiance(combatant, allegiance);
        }
        return combatant;
    }

    private static Combatant CloneWithAllegiance(Combatant c, Allegiance newAllegiance)
    {
        // Combatant has no copy-with API; for the duel tool we just rebuild from
        // the source template with the overridden allegiance baked in. The template
        // is the only path that produces NPC combatants in this command.
        var def = c.SourceTemplate!;
        // Fake the template's default_allegiance with a mutated copy (records support `with`).
        var swapped = def with { DefaultAllegiance = newAllegiance.ToString().ToLowerInvariant() };
        return Combatant.FromNpcTemplate(swapped, c.Id, c.Position);
    }

    private static Character BuildLevel1Character(
        ContentResolver content, string cladeId, string speciesId, string classId, string bgId)
    {
        if (!content.Clades.TryGetValue(cladeId, out var clade))
            throw new System.ArgumentException($"Unknown clade: {cladeId}");
        if (!content.Species.TryGetValue(speciesId, out var species))
            throw new System.ArgumentException($"Unknown species: {speciesId}");
        if (!content.Classes.TryGetValue(classId, out var classDef))
            throw new System.ArgumentException($"Unknown class: {classId}");
        if (!content.Backgrounds.TryGetValue(bgId, out var bg))
            throw new System.ArgumentException($"Unknown background: {bgId}");

        // Standard array assigned by class priority.
        int[] vals = (int[])AbilityScores.StandardArray.Clone();
        System.Array.Sort(vals, (a, b) => b - a);
        var primary = classDef.PrimaryAbility ?? System.Array.Empty<string>();
        var order = new List<string>();
        foreach (var p in primary) order.Add(p.ToUpperInvariant());
        foreach (var a in new[] { "CON", "DEX", "STR", "WIS", "INT", "CHA" })
            if (!order.Contains(a)) order.Add(a);
        var assigned = new Dictionary<string, int>();
        for (int i = 0; i < 6; i++) assigned[order[i]] = vals[i];

        var b = new CharacterBuilder
        {
            Clade        = clade,
            Species      = species,
            ClassDef     = classDef,
            Background   = bg,
            BaseAbilities = new AbilityScores(
                assigned["STR"], assigned["DEX"], assigned["CON"],
                assigned["INT"], assigned["WIS"], assigned["CHA"]),
            Name         = $"{species.Name} {classDef.Name}",
        };
        int n = classDef.SkillsChoose;
        foreach (var raw in classDef.SkillOptions)
        {
            if (b.ChosenClassSkills.Count >= n) break;
            try { b.ChosenClassSkills.Add(SkillIdExtensions.FromJson(raw)); } catch { }
        }
        return b.Build(content.Items);
    }
}
