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

namespace Theriapolis.Core.Rules.Character;

/// <summary>
/// Fluent builder for a level-1 <see cref="Character"/>. Used by both the
/// in-game character-creation screen and the headless <c>character-roll</c>
/// Tools command, plus the M2 test suite.
///
/// Pattern: set inputs (clade, species, class, background, base scores,
/// chosen skills, name), then call <see cref="Build"/>. <see cref="Validate"/>
/// returns the first error string when any required input is missing or
/// inconsistent — <see cref="Build"/> calls Validate and throws on failure.
/// </summary>
public sealed class CharacterBuilder
{
    public CladeDef?      Clade      { get; set; }
    public SpeciesDef?    Species    { get; set; }
    public ClassDef?      ClassDef   { get; set; }
    public BackgroundDef? Background { get; set; }

    /// <summary>Pre-clade-mod base scores (e.g. Standard Array assignment or 4d6 roll outcome).</summary>
    public AbilityScores BaseAbilities { get; set; } = new(10, 10, 10, 10, 10, 10);

    /// <summary>Class-skill picks. Background skills are added automatically by Build().</summary>
    public HashSet<SkillId> ChosenClassSkills { get; } = new();

    public string Name { get; set; } = "Wanderer";

    // ── Builder fluent helpers ──────────────────────────────────────────

    public CharacterBuilder WithClade(CladeDef c)           { Clade      = c; return this; }
    public CharacterBuilder WithSpecies(SpeciesDef s)       { Species    = s; return this; }
    public CharacterBuilder WithClass(ClassDef c)           { ClassDef   = c; return this; }
    public CharacterBuilder WithBackground(BackgroundDef b) { Background = b; return this; }
    public CharacterBuilder WithAbilities(AbilityScores a)  { BaseAbilities = a; return this; }
    public CharacterBuilder WithName(string name)           { Name = name ?? "Wanderer"; return this; }

    public CharacterBuilder ChooseSkill(SkillId s)
    {
        ChosenClassSkills.Add(s);
        return this;
    }

    // ── Validation ──────────────────────────────────────────────────────

    public bool Validate(out string error)
    {
        error = "";
        if (Clade is null)      { error = "Clade not selected.";      return false; }
        if (Species is null)    { error = "Species not selected.";    return false; }
        if (ClassDef is null)   { error = "Class not selected.";      return false; }
        if (Background is null) { error = "Background not selected."; return false; }

        if (!string.Equals(Species.CladeId, Clade.Id, StringComparison.OrdinalIgnoreCase))
        {
            error = $"Species '{Species.Id}' belongs to clade '{Species.CladeId}', not '{Clade.Id}'.";
            return false;
        }

        // Validate every chosen class skill is in the class's offered list.
        foreach (var s in ChosenClassSkills)
        {
            string raw = SkillToJsonName(s);
            bool listed = false;
            foreach (var opt in ClassDef.SkillOptions)
                if (string.Equals(opt, raw, StringComparison.OrdinalIgnoreCase))
                {
                    listed = true;
                    break;
                }
            if (!listed)
            {
                error = $"Class '{ClassDef.Id}' does not offer skill '{raw}'.";
                return false;
            }
        }

        if (ChosenClassSkills.Count != ClassDef.SkillsChoose)
        {
            error = $"Class '{ClassDef.Id}' requires {ClassDef.SkillsChoose} skill picks, got {ChosenClassSkills.Count}.";
            return false;
        }

        return true;
    }

    // ── Build ───────────────────────────────────────────────────────────

    public Character Build(IReadOnlyDictionary<string, ItemDef>? itemsForStartingKit = null)
    {
        if (!Validate(out string error))
            throw new InvalidOperationException($"Cannot build character: {error}");

        // Apply clade + species ability mods to the base scores.
        var clade   = Clade!;
        var species = Species!;
        var classD  = ClassDef!;
        var bgD     = Background!;

        var modded = BaseAbilities;
        modded = ApplyMods(modded, clade.AbilityMods);
        modded = ApplyMods(modded, species.AbilityMods);

        var c = new Character(clade, species, classD, bgD, modded)
        {
            Level = 1,
            Xp    = 0,
        };

        // Skills: class-chosen + background freebies (deduplicated).
        foreach (var s in ChosenClassSkills) c.SkillProficiencies.Add(s);
        foreach (var raw in bgD.SkillProficiencies)
        {
            try { c.SkillProficiencies.Add(SkillIdExtensions.FromJson(raw)); }
            catch (ArgumentException) { /* unknown skill names ignored — content bug, but don't crash creation */ }
        }

        // HP: HitDie + CON modifier at level 1.
        c.MaxHp     = c.ComputeMaxHpFromScratch();
        c.CurrentHp = c.MaxHp;

        // Optional starting kit. The caller passes the loaded item table
        // (typically <see cref="Data.ContentResolver.Items"/>); if null, the
        // character starts with an empty inventory (existing test behaviour).
        if (itemsForStartingKit is not null)
            ApplyStartingKit(c, itemsForStartingKit);

        return c;
    }

    /// <summary>
    /// Adds every entry from <see cref="ClassDef.StartingKit"/> to the
    /// character's inventory and auto-equips entries flagged for it. Logs and
    /// continues on missing items / unknown slots — content bugs should fail
    /// loud at content-validate time, not crash character creation.
    /// </summary>
    public static void ApplyStartingKit(Character c, IReadOnlyDictionary<string, ItemDef> items)
    {
        foreach (var entry in c.ClassDef.StartingKit)
        {
            if (!items.TryGetValue(entry.ItemId, out var def))
                continue;  // unknown item id — skip silently (caught by ContentValidate)

            var inst = c.Inventory.Add(def, Math.Max(1, entry.Qty));
            if (!entry.AutoEquip || string.IsNullOrEmpty(entry.EquipSlot))
                continue;

            var slot = EquipSlotExtensions.FromJson(entry.EquipSlot);
            if (slot is null) continue;
            // Best-effort equip; ignore the error string here. If a structural
            // conflict occurs (two-handed in main when off-hand pre-occupied),
            // the item stays in the bag rather than blocking creation.
            c.Inventory.TryEquip(inst, slot.Value, out _);
        }
    }

    private static AbilityScores ApplyMods(AbilityScores a, IReadOnlyDictionary<string, int> mods)
    {
        if (mods is null || mods.Count == 0) return a;
        var dict = new Dictionary<AbilityId, int>();
        foreach (var kv in mods)
        {
            if (TryParseAbility(kv.Key, out var id))
                dict[id] = (dict.TryGetValue(id, out var existing) ? existing : 0) + kv.Value;
        }
        return a.Plus(dict);
    }

    private static bool TryParseAbility(string raw, out AbilityId id)
    {
        switch (raw.ToUpperInvariant())
        {
            case "STR": id = AbilityId.STR; return true;
            case "DEX": id = AbilityId.DEX; return true;
            case "CON": id = AbilityId.CON; return true;
            case "INT": id = AbilityId.INT; return true;
            case "WIS": id = AbilityId.WIS; return true;
            case "CHA": id = AbilityId.CHA; return true;
            default:    id = AbilityId.STR; return false;
        }
    }

    private static string SkillToJsonName(SkillId s) => s switch
    {
        SkillId.Acrobatics      => "acrobatics",
        SkillId.AnimalHandling  => "animal_handling",
        SkillId.Arcana          => "arcana",
        SkillId.Athletics       => "athletics",
        SkillId.Deception       => "deception",
        SkillId.History         => "history",
        SkillId.Insight         => "insight",
        SkillId.Intimidation    => "intimidation",
        SkillId.Investigation   => "investigation",
        SkillId.Medicine        => "medicine",
        SkillId.Nature          => "nature",
        SkillId.Perception      => "perception",
        SkillId.Performance     => "performance",
        SkillId.Persuasion      => "persuasion",
        SkillId.Religion        => "religion",
        SkillId.SleightOfHand   => "sleight_of_hand",
        SkillId.Stealth         => "stealth",
        SkillId.Survival        => "survival",
        _ => s.ToString().ToLowerInvariant(),
    };

    // ── Stat-rolling ────────────────────────────────────────────────────

    /// <summary>
    /// Roll 4d6-drop-lowest six times, returning a fresh <see cref="AbilityScores"/>
    /// in (STR, DEX, CON, INT, WIS, CHA) order. Player assigns afterward.
    ///
    /// Seed:
    ///   <c>worldSeed ^ C.RNG_STAT_ROLL ^ msSinceGameStart</c>
    /// where <paramref name="msSinceGameStart"/> is wall-clock ms since process
    /// launch in production, or a fixed test override for reproducibility.
    /// </summary>
    public static AbilityScores RollAbilityScores(ulong worldSeed, ulong msSinceGameStart)
    {
        var rng = SeededRng.ForSubsystem(worldSeed, C.RNG_STAT_ROLL ^ msSinceGameStart);
        int[] r = new int[6];
        for (int i = 0; i < 6; i++) r[i] = Roll4d6DropLowest(rng);
        return new AbilityScores(r[0], r[1], r[2], r[3], r[4], r[5]);
    }

    /// <summary>4d6, drop the lowest; returns 3..18.</summary>
    public static int Roll4d6DropLowest(SeededRng rng)
    {
        int d1 = (int)(rng.NextUInt64() % 6) + 1;
        int d2 = (int)(rng.NextUInt64() % 6) + 1;
        int d3 = (int)(rng.NextUInt64() % 6) + 1;
        int d4 = (int)(rng.NextUInt64() % 6) + 1;
        int low = Math.Min(d1, Math.Min(d2, Math.Min(d3, d4)));
        return d1 + d2 + d3 + d4 - low;
    }
}
