using Theriapolis.Core.Rules.Stats;
using Theriapolis.Core.Util;

namespace Theriapolis.Core.Rules.Character;

/// <summary>
/// Phase 6.5 M0 — the level-up flow.
///
/// <see cref="Compute"/> is a pure function: given a character, a target
/// level, and a deterministic seed, it produces a <see cref="LevelUpResult"/>
/// describing what the level-up *would* do without mutating anything. The
/// level-up screen previews this; <see cref="Character.ApplyLevelUp"/>
/// commits it.
///
/// Determinism: <c>seed = worldSeed ^ characterCreationMs ^ RNG_LEVELUP ^ targetLevel</c>.
/// Same seed → same HP roll, same feature list, same ASI/subclass slots open.
/// Save mid-flow → load → re-compute produces byte-identical payload.
/// </summary>
public static class LevelUpFlow
{
    /// <summary>
    /// True when the character has accumulated enough XP to level up.
    /// Wraps the <see cref="XpTable"/> threshold check.
    /// </summary>
    public static bool CanLevelUp(Character character)
    {
        if (character.Level >= C.CHARACTER_LEVEL_MAX) return false;
        return character.Xp >= XpTable.XpRequiredForNextLevel(character.Level);
    }

    /// <summary>
    /// Compute (but do not apply) the level-up payload for advancing
    /// <paramref name="character"/> to <paramref name="targetLevel"/>.
    /// Caller is expected to validate <c>targetLevel == character.Level + 1</c>;
    /// no in-method assertion so unit tests can roll forward without mutation.
    ///
    /// <paramref name="seed"/> determines HP roll outcome when the player
    /// chooses to roll instead of take average. Seeded callers should pass
    /// <c>worldSeed ^ characterCreationMs ^ C.RNG_LEVELUP ^ (ulong)targetLevel</c>.
    ///
    /// <paramref name="subclasses"/> is the content resolver's subclass
    /// dictionary; when non-null and the character has a chosen subclass,
    /// the result's <see cref="LevelUpResult.SubclassFeaturesUnlocked"/>
    /// is populated. Phase 6.5 M0 callers (and tests without content) pass
    /// null, which yields an empty subclass-feature list.
    /// </summary>
    public static LevelUpResult Compute(
        Character character,
        int       targetLevel,
        ulong     seed,
        bool      takeAverage = true,
        IReadOnlyDictionary<string, Data.SubclassDef>? subclasses = null)
    {
        var classDef = character.ClassDef;
        int conMod   = character.Abilities.ModFor(AbilityId.CON);

        int hitDie  = classDef.HitDie;
        int avgHp   = (hitDie / 2) + 1;
        int rollHp;
        if (takeAverage)
        {
            rollHp = avgHp;
        }
        else
        {
            // Roll 1d{hitDie} from a fresh stream so the result is reproducible
            // per-call. Use SeededRng to match Phase 5/6 RNG conventions.
            var rng = new SeededRng(seed);
            rollHp = (int)(rng.NextUInt64() % (uint)hitDie) + 1;
        }
        int hpGained = Math.Max(1, rollHp + conMod);

        // Class features at this level. The level-table indexes by Level
        // (1-based); arrays are 0-based so look up at [level - 1].
        var classFeatures = Array.Empty<string>();
        if (targetLevel >= 1 && targetLevel <= classDef.LevelTable.Length)
        {
            var entry = classDef.LevelTable[targetLevel - 1];
            classFeatures = entry.Features ?? Array.Empty<string>();
        }

        // Phase 6.5 M2 — resolve subclass features from the chosen subclass
        // (post-L3). When `subclasses` is null (M0 callers / tests without
        // content), no subclass features are unlocked.
        string[] subclassFeatures = subclasses is not null && !string.IsNullOrEmpty(character.SubclassId)
            ? SubclassResolver.UnlockedFeaturesAt(subclasses, character.SubclassId, targetLevel)
            : Array.Empty<string>();

        bool grantsSubclass = targetLevel == C.SUBCLASS_SELECTION_LEVEL
                              && string.IsNullOrEmpty(character.SubclassId)
                              && classDef.SubclassIds.Length > 0;
        bool grantsAsi      = Array.IndexOf(C.ASI_LEVELS, targetLevel) >= 0;

        return new LevelUpResult
        {
            NewLevel                 = targetLevel,
            HpGained                 = hpGained,
            HpHitDieResult           = rollHp,
            HpWasAveraged            = takeAverage,
            ClassFeaturesUnlocked    = classFeatures,
            SubclassFeaturesUnlocked = subclassFeatures,
            GrantsSubclassChoice     = grantsSubclass,
            GrantsAsiChoice          = grantsAsi,
            NewProficiencyBonus      = ProficiencyBonus.ForLevel(targetLevel),
        };
    }
}
