using Theriapolis.Core.Util;

namespace Theriapolis.Core.Rules.Combat;

/// <summary>
/// One combat encounter. Owns the participants, initiative order, current
/// turn pointer, log, and a per-encounter <see cref="SeededRng"/> seeded
/// from <c>worldSeed ^ C.RNG_COMBAT ^ encounterId</c>. Save/load can resume
/// mid-combat by capturing <see cref="EncounterSeed"/> +
/// <see cref="RollCount"/> and replaying the dice stream from the same
/// sequence point — see <see cref="ResumeRolls"/>.
/// </summary>
public sealed class Encounter
{
    public ulong EncounterId   { get; }
    public ulong EncounterSeed { get; }
    public IReadOnlyList<Combatant> Participants => _participants;
    public IReadOnlyList<int> InitiativeOrder    => _initiativeOrder;
    public int    CurrentTurnIndex { get; private set; }
    public int    RoundNumber      { get; private set; } = 1;
    public Turn   CurrentTurn      { get; private set; }
    public IReadOnlyList<CombatLogEntry> Log => _log;
    public bool   IsOver           => _isOver;

    /// <summary>How many dice rolls have been drawn from this encounter's RNG.</summary>
    public int    RollCount        { get; private set; }

    private readonly List<Combatant> _participants;
    private readonly List<int>       _initiativeOrder;
    private readonly List<CombatLogEntry> _log = new();
    private SeededRng _rng;
    private bool _isOver;

    public Encounter(ulong worldSeed, ulong encounterId, IEnumerable<Combatant> combatants)
    {
        EncounterId   = encounterId;
        EncounterSeed = worldSeed ^ C.RNG_COMBAT ^ encounterId;
        _rng          = new SeededRng(EncounterSeed);
        _participants = new List<Combatant>(combatants);
        if (_participants.Count == 0)
            throw new System.ArgumentException("Encounter requires at least one combatant.", nameof(combatants));

        _initiativeOrder = RollInitiative();
        CurrentTurnIndex = 0;
        CurrentTurn      = Turn.FreshFor(CurrentActor.Id, CurrentActor.SpeedFt);
        AppendLog(CombatLogEntry.Kind.Initiative, FormatInitiativeOrder());
        AppendLog(CombatLogEntry.Kind.TurnStart,  $"Round 1 — {CurrentActor.Name}'s turn.");
    }

    public Combatant CurrentActor => _participants[_initiativeOrder[CurrentTurnIndex]];

    public Combatant? GetById(int id)
    {
        foreach (var c in _participants) if (c.Id == id) return c;
        return null;
    }

    /// <summary>
    /// Advances to the next living combatant. Wraps the round counter when
    /// we cycle past the last initiative slot. Marks the encounter over if
    /// only one allegiance has living combatants.
    /// </summary>
    public void EndTurn()
    {
        if (_isOver) return;

        int n = _initiativeOrder.Count;
        for (int step = 0; step < n; step++)
        {
            CurrentTurnIndex++;
            if (CurrentTurnIndex >= n)
            {
                CurrentTurnIndex = 0;
                RoundNumber++;
            }
            var next = CurrentActor;
            if (next.IsAlive)
            {
                CurrentTurn = Turn.FreshFor(next.Id, next.SpeedFt);
                next.OnTurnStart();  // Phase 5 M6: reset per-turn feature flags (Sneak Attack)
                AppendLog(CombatLogEntry.Kind.TurnStart, $"Round {RoundNumber} — {next.Name}'s turn.");
                CheckForVictory();
                return;
            }
        }
        // No one is alive.
        EndEncounter("No combatants remain.");
    }

    /// <summary>
    /// Returns true and ends the encounter if only one allegiance has
    /// living combatants left. Called automatically at end-of-turn.
    /// </summary>
    public bool CheckForVictory()
    {
        var living = new HashSet<Rules.Character.Allegiance>();
        foreach (var c in _participants)
            if (c.IsAlive && !c.IsDown) living.Add(c.Allegiance);

        // Allies and Players count as the same side for victory purposes.
        bool playerSide  = living.Contains(Rules.Character.Allegiance.Player) || living.Contains(Rules.Character.Allegiance.Allied);
        bool hostileSide = living.Contains(Rules.Character.Allegiance.Hostile);

        if (!playerSide || !hostileSide)
        {
            string verdict = playerSide ? "Player side wins." : (hostileSide ? "Hostile side wins." : "Mutual annihilation.");
            EndEncounter(verdict);
            return true;
        }
        return false;
    }

    private void EndEncounter(string verdict)
    {
        _isOver = true;
        AppendLog(CombatLogEntry.Kind.EncounterEnd, $"Encounter ends after {RoundNumber} round(s). {verdict}");
    }

    // ── Dice ──────────────────────────────────────────────────────────────

    /// <summary>
    /// Draw a uniform integer in [1, sides]. Increments
    /// <see cref="RollCount"/>; save/load uses that count to resume.
    /// </summary>
    public int RollDie(int sides)
    {
        if (sides < 1) return 0;
        RollCount++;
        return (int)(_rng.NextUInt64() % (ulong)sides) + 1;
    }

    public int RollD20() => RollDie(20);

    /// <summary>
    /// Roll d20 with advantage (best of two) or disadvantage (worst of two).
    /// Returns (kept, other) so the caller can log both.
    /// </summary>
    public (int kept, int other) RollD20WithMode(SituationFlags flags)
    {
        if (flags.RollsAdvantage())
        {
            int a = RollD20();
            int b = RollD20();
            return a >= b ? (a, b) : (b, a);
        }
        if (flags.RollsDisadvantage())
        {
            int a = RollD20();
            int b = RollD20();
            return a <= b ? (a, b) : (b, a);
        }
        return (RollD20(), -1);
    }

    /// <summary>
    /// Re-create the RNG and skip <paramref name="rollCount"/> rolls.
    /// Used by the save layer to resume mid-combat encounters: capture
    /// (encounterId, rollCount) on save; recreate Encounter with same
    /// participants and call ResumeRolls(savedRollCount) on load.
    /// </summary>
    public void ResumeRolls(int rollCount)
    {
        _rng = new SeededRng(EncounterSeed);
        for (int i = 0; i < rollCount; i++) _rng.NextUInt64();
        RollCount = rollCount;
    }

    // ── Logging ───────────────────────────────────────────────────────────

    public void AppendLog(CombatLogEntry.Kind kind, string message)
    {
        _log.Add(new CombatLogEntry
        {
            Round = RoundNumber,
            Turn  = CurrentTurnIndex,
            Type  = kind,
            Message = message,
        });
    }

    // ── Initiative ────────────────────────────────────────────────────────

    private List<int> RollInitiative()
    {
        var rolls = new (int idx, int total, int initBonus, int dexMod)[_participants.Count];
        for (int i = 0; i < _participants.Count; i++)
        {
            var c = _participants[i];
            int d20 = RollD20();
            rolls[i] = (i, d20 + c.InitiativeBonus, c.InitiativeBonus,
                        Stats.AbilityScores.Mod(c.Abilities.DEX));
        }
        // Sort descending by total; ties broken by DEX mod descending; final tiebreaker by id ascending.
        System.Array.Sort(rolls, (a, b) =>
        {
            int byTotal = b.total.CompareTo(a.total);
            if (byTotal != 0) return byTotal;
            int byDex   = b.dexMod.CompareTo(a.dexMod);
            if (byDex   != 0) return byDex;
            return _participants[a.idx].Id.CompareTo(_participants[b.idx].Id);
        });
        var order = new List<int>(rolls.Length);
        foreach (var r in rolls) order.Add(r.idx);
        return order;
    }

    private string FormatInitiativeOrder()
    {
        var parts = new List<string>(_initiativeOrder.Count);
        foreach (int idx in _initiativeOrder)
        {
            var c = _participants[idx];
            parts.Add($"{c.Name} (init+{c.InitiativeBonus})");
        }
        return "Initiative: " + string.Join(", ", parts);
    }
}
