using Theriapolis.Core.Data;
using Theriapolis.Core.Items;
using Theriapolis.Core.Rules.Stats;

namespace Theriapolis.Core.Rules.Character;

/// <summary>
/// Runtime aggregate of everything that makes a creature a *character* —
/// stats, class, clade, species, background, inventory, HP, conditions, level,
/// XP. Composed onto an <see cref="Entities.Actor"/> via the
/// <c>Actor.Character</c> field; the actor handles position and rendering,
/// the character handles gameplay state.
///
/// Phase 5 M2 builds these via <see cref="CharacterBuilder"/> at character
/// creation. Saved snapshots round-trip through
/// <see cref="Persistence.PlayerCharacterState"/>.
/// </summary>
public sealed class Character
{
    public CladeDef       Clade      { get; }
    public SpeciesDef     Species    { get; }
    public ClassDef       ClassDef   { get; }
    public BackgroundDef  Background { get; }
    public AbilityScores  Abilities  { get; private set; }

    public int    Level         { get; set; } = 1;
    public int    Xp            { get; set; } = 0;
    public int    MaxHp         { get; set; }
    public int    CurrentHp     { get; set; }

    /// <summary>
    /// Phase 6.5 M0 — subclass id, set when the level-3 selection is made.
    /// Empty pre-L3; references one of <see cref="ClassDef.SubclassIds"/> after.
    /// Loaded by save round-trip.
    /// </summary>
    public string SubclassId    { get; set; } = "";

    /// <summary>
    /// Phase 6.5 M0 — feature ids learned across all level-ups, in unlock
    /// order. Includes level-1 features applied by <see cref="CharacterBuilder"/>
    /// plus everything <see cref="ApplyLevelUp"/> appends. The
    /// <see cref="FeatureProcessor"/> consults this list when resolving combat
    /// effects, dialogue hooks, etc.
    /// </summary>
    public List<string> LearnedFeatureIds { get; } = new();

    /// <summary>
    /// Phase 6.5 M0 — append-only history of per-level deltas. Used by
    /// the level-up screen for display and by save round-trip for
    /// reproducibility. Index = level - 1 (so [0] is the level-1 entry,
    /// [1] is the level-2 entry, etc.); element 0 is synthesized at
    /// character creation.
    /// </summary>
    public List<LevelUpRecord> LevelUpHistory { get; } = new();

    /// <summary>
    /// Phase 6.5 M4 — hybrid-character state. Null for purebred PCs
    /// (the common case); non-null indicates the PC was built via
    /// <see cref="CharacterBuilder.TryBuildHybrid"/>. Universal hybrid
    /// detriments (Scent Dysphoria, Social Stigma, Illegible Body
    /// Language, Medical Incompatibility) are inherent and applied via
    /// <see cref="HybridDetriments"/> at use sites.
    /// </summary>
    public HybridState? Hybrid { get; set; }

    /// <summary>True when the PC is a hybrid (i.e. <see cref="Hybrid"/> non-null).</summary>
    public bool IsHybrid => Hybrid is not null;

    /// <summary>Skill proficiencies — class skills + background skills, deduplicated.</summary>
    public HashSet<SkillId> SkillProficiencies { get; } = new();

    public Inventory Inventory { get; } = new();

    /// <summary>Conditions currently affecting the character. Phase 5 M5 wires durations.</summary>
    public HashSet<Condition> Conditions { get; } = new();

    /// <summary>Exhaustion level 0..6, separate from <see cref="Conditions"/> (binary flags).</summary>
    public int ExhaustionLevel { get; set; } = 0;

    /// <summary>
    /// Phase 5 M6: chosen Fangsworn fighting style ("duelist", "great_weapon",
    /// "shieldwall", "fang_and_blade", "natural_predator"). Empty for non-Fangsworn
    /// or when not yet picked. Defaults to "duelist" via CharacterBuilder.
    /// </summary>
    public string FightingStyle { get; set; } = "";

    /// <summary>Phase 5 M6: Feral Rage uses remaining (refills on long rest; M6 treats as per-encounter).</summary>
    public int RageUsesRemaining { get; set; } = 2;

    // ── Phase 6.5 M1: per-encounter resource pools ────────────────────────
    /// <summary>
    /// Muzzle-Speaker Vocalization Dice — uses remaining (long-rest, 4 default
    /// at level 1). Refilled per encounter at M1 since the rest model lives
    /// in Phase 8.
    /// </summary>
    public int VocalizationDiceRemaining { get; set; } = 4;

    /// <summary>
    /// Covenant-Keeper Lay on Paws — HP pool remaining. Recharges to
    /// <c>5 × CHA</c> on long rest; M1 refills per encounter.
    /// </summary>
    public int LayOnPawsPoolRemaining { get; set; }

    /// <summary>
    /// Claw-Wright Field Repair — uses remaining. Once per short rest at L1
    /// per the JSON; M1 treats as 1 per encounter.
    /// </summary>
    public int FieldRepairUsesRemaining { get; set; } = 1;

    // ── Phase 6.5 M3: ability-stream resource pools ──────────────────────
    /// <summary>
    /// Scent-Broker Pheromone Craft — uses remaining. The JSON ladder
    /// (<c>pheromone_craft_2/3/4/5</c> at L2/L5/L9/L13) sets the per-rest
    /// cap; <see cref="Combat.FeatureProcessor.EnsurePheromoneUsesReady"/>
    /// tops the pool up to that cap at encounter start.
    /// </summary>
    public int PheromoneUsesRemaining { get; set; }

    /// <summary>
    /// Covenant-Keeper Covenant's Authority — uses remaining. The JSON
    /// ladder (<c>covenants_authority_2/3/4/5</c> at L2/L9/L13/L17) sets
    /// the cap; M3 tops up per encounter.
    /// </summary>
    public int CovenantAuthorityUsesRemaining { get; set; }

    /// <summary>
    /// Phase 6 M3 — Fangs (Theriapolis's universal coin). Used by the shop
    /// dialogue branch. Defaults to a small starting stipend so the very
    /// first merchant interaction has something to buy with.
    /// </summary>
    public int CurrencyFang { get; set; } = 25;

    public bool IsAlive => CurrentHp > 0 || Conditions.Contains(Condition.Unconscious);

    public Character(
        CladeDef clade,
        SpeciesDef species,
        ClassDef classDef,
        BackgroundDef background,
        AbilityScores abilities)
    {
        Clade      = clade      ?? throw new ArgumentNullException(nameof(clade));
        Species    = species    ?? throw new ArgumentNullException(nameof(species));
        ClassDef   = classDef   ?? throw new ArgumentNullException(nameof(classDef));
        Background = background ?? throw new ArgumentNullException(nameof(background));
        Abilities  = abilities;
    }

    /// <summary>Replace ability scores wholesale (used during creation; combat doesn't mutate this).</summary>
    public void SetAbilities(AbilityScores scores)
    {
        Abilities = scores;
    }

    /// <summary>Body size category, derived from species.</summary>
    public SizeCategory Size => SizeExtensions.FromJson(Species.Size);

    /// <summary>d20 proficiency bonus for the character's current level.</summary>
    public int ProficiencyBonus => Stats.ProficiencyBonus.ForLevel(Level);

    /// <summary>
    /// Computes max HP from class hit die + CON modifier at level 1 (and
    /// avg-rounded-up + CON for each level beyond, per d20 default).
    /// Phase 6.5 M0: still useful for character-creation initial HP, but
    /// real per-level HP gains come from <see cref="ApplyLevelUp"/>'s
    /// <see cref="LevelUpResult.HpGained"/> deltas (which respect the
    /// player's roll-vs-average choice and pin to a deterministic seed).
    /// </summary>
    public int ComputeMaxHpFromScratch()
    {
        int conMod = Abilities.ModFor(AbilityId.CON);
        int hp = ClassDef.HitDie + conMod;
        // Levels 2+ add avg-rounded-up + CON each. Phase 5 = level 1 only,
        // but the formula stays correct in case external code passes Level > 1.
        for (int lv = 2; lv <= Level; lv++)
        {
            int avgRoundedUp = (ClassDef.HitDie / 2) + 1;
            hp += avgRoundedUp + conMod;
        }
        return Math.Max(1, hp);
    }

    /// <summary>
    /// Phase 6.5 M0 — apply a previously-computed <see cref="LevelUpResult"/>
    /// plus the player's <see cref="LevelUpChoices"/> to this character.
    /// Mutates Level, MaxHp, CurrentHp, SubclassId, Abilities, and appends
    /// the unlocked features to <see cref="LearnedFeatureIds"/>. Records
    /// the event in <see cref="LevelUpHistory"/>.
    ///
    /// The caller is responsible for verifying the choices are valid for
    /// the result's open slots (subclass selected when GrantsSubclassChoice;
    /// ASI sums to +2 when GrantsAsiChoice). Validation lives in
    /// <see cref="LevelUpChoicesValidator"/>; this method trusts what it
    /// gets so it can be called from tests with bare choices.
    /// </summary>
    public void ApplyLevelUp(LevelUpResult result, LevelUpChoices choices)
    {
        if (result is null)  throw new ArgumentNullException(nameof(result));
        if (choices is null) throw new ArgumentNullException(nameof(choices));

        // Apply ASI (if any).
        if (result.GrantsAsiChoice && choices.AsiAdjustments.Count > 0)
        {
            var newAbilities = Abilities;
            foreach (var (ability, delta) in choices.AsiAdjustments)
            {
                int current = newAbilities.Get(ability);
                int cap = result.NewLevel >= C.CHARACTER_LEVEL_MAX
                    ? C.ABILITY_SCORE_CAP_AT_L20
                    : C.ABILITY_SCORE_CAP_PRE_L20;
                int next = Math.Min(cap, current + delta);
                newAbilities = newAbilities.With(ability, next);
            }
            Abilities = newAbilities;
        }

        // Subclass selection (if any).
        if (result.GrantsSubclassChoice && !string.IsNullOrEmpty(choices.SubclassId))
        {
            SubclassId = choices.SubclassId!;
        }

        // HP. The result's HpGained already incorporates CON mod at compute
        // time; if the player took CON via ASI on the same level-up, we use
        // the *new* CON for HP gained. Recompute defensively.
        int conMod = Abilities.ModFor(AbilityId.CON);
        int hpGained;
        if (result.HpWasAveraged)
        {
            int avgRoundedUp = (ClassDef.HitDie / 2) + 1;
            hpGained = Math.Max(1, avgRoundedUp + conMod);
        }
        else
        {
            // Roll value already determined; but CON might have changed.
            hpGained = Math.Max(1, result.HpHitDieResult + conMod);
        }
        MaxHp     += hpGained;
        CurrentHp += hpGained;     // level-up restores per d20 default

        // Learned features.
        foreach (var fid in result.ClassFeaturesUnlocked)
            LearnedFeatureIds.Add(fid);
        foreach (var fid in result.SubclassFeaturesUnlocked)
            LearnedFeatureIds.Add(fid);

        // Level — last, so all the per-level computations above use the
        // pre-level-up Level for any branching they need.
        Level = result.NewLevel;

        // Record the event.
        LevelUpHistory.Add(new LevelUpRecord
        {
            Level                = result.NewLevel,
            HpGained             = hpGained,
            HpWasAveraged        = result.HpWasAveraged,
            HpHitDieResult       = result.HpHitDieResult,
            SubclassChosen       = result.GrantsSubclassChoice ? choices.SubclassId : null,
            AsiAdjustmentsKeys   = choices.AsiAdjustments.Keys.Select(k => (byte)k).ToArray(),
            AsiAdjustmentsValues = choices.AsiAdjustments.Values.ToArray(),
            FeaturesUnlocked     = result.ClassFeaturesUnlocked
                                       .Concat(result.SubclassFeaturesUnlocked)
                                       .ToArray(),
        });
    }
}

/// <summary>
/// Phase 6.5 M0 — one entry in <see cref="Character.LevelUpHistory"/>.
/// Plain-data, serializable. Records the *deltas* (not the post-state), so
/// the history can be replayed on load and the level-up screen can show
/// the player what happened at each level.
/// </summary>
public sealed class LevelUpRecord
{
    public int Level { get; init; }
    public int HpGained { get; init; }
    public bool HpWasAveraged { get; init; }
    public int HpHitDieResult { get; init; }
    public string? SubclassChosen { get; init; }
    public byte[] AsiAdjustmentsKeys   { get; init; } = Array.Empty<byte>();
    public int[]  AsiAdjustmentsValues { get; init; } = Array.Empty<int>();
    public string[] FeaturesUnlocked { get; init; } = Array.Empty<string>();
}
