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";

    /// <summary>
    /// Phase 5 M6: Fangsworn fighting style choice. One of "duelist",
    /// "great_weapon", "shieldwall", "fang_and_blade", "natural_predator".
    /// Empty string defaults to "duelist" if the class is Fangsworn (sensible
    /// auto-pick that has visible combat effect at level 1). Ignored for
    /// non-Fangsworn classes.
    /// </summary>
    public string FightingStyle { get; set; } = "";

    // ── Phase 6.5 M4: hybrid origin ─────────────────────────────────────
    /// <summary>
    /// When true, <see cref="TryBuildHybrid"/> is the canonical build path
    /// and <see cref="Clade"/> / <see cref="Species"/> are the *dominant*
    /// parent's lineage; <see cref="HybridSireClade"/> /
    /// <see cref="HybridDamClade"/> populate the secondary parent. Defaults
    /// to false (purebred path); the character creation screen flips this
    /// when the player ticks the Hybrid checkbox.
    /// </summary>
    public bool IsHybridOrigin { get; set; } = false;

    /// <summary>Sire clade for hybrid origin path (paternal lineage).</summary>
    public CladeDef?   HybridSireClade   { get; set; }
    /// <summary>Sire species for hybrid origin path.</summary>
    public SpeciesDef? HybridSireSpecies { get; set; }
    /// <summary>Dam clade for hybrid origin path (maternal lineage).</summary>
    public CladeDef?   HybridDamClade    { get; set; }
    /// <summary>Dam species for hybrid origin path.</summary>
    public SpeciesDef? HybridDamSpecies  { get; set; }

    /// <summary>
    /// Which parent's expression dominates. Drives Passing presentation
    /// (the PC scent-reads as this lineage's clade). Default is Sire.
    /// </summary>
    public ParentLineage HybridDominantParent { get; set; } = ParentLineage.Sire;

    /// <summary>
    /// Sire's chosen ability key ("STR", "DEX", "CON", "INT", "WIS", "CHA").
    /// Hybrid PCs take ONE ability mod from each parent clade — this records
    /// which one the player chose from sire's clade. Empty string means no
    /// bonus from this side (graceful default for headless builds and old
    /// content). The resulting mod value is whatever the sire clade grants
    /// for that ability (e.g. picking CON from canidae yields +1, picking
    /// CON from ursidae yields +2). Stacks additively with the dam pick if
    /// both happen to land on the same ability.
    /// </summary>
    public string HybridSireChosenAbility { get; set; } = "";

    /// <summary>Dam's chosen ability key — see <see cref="HybridSireChosenAbility"/>.</summary>
    public string HybridDamChosenAbility { get; set; } = "";

    // ── 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;

        // Phase 5 M6: Fangsworn fighting style. Default to "duelist" — has
        // immediate combat effect at level 1 and works with the most weapons
        // in our starting kits. The CodexUI character creator surfaces this
        // as a real picker; the legacy Myra screen leaves it on default.
        if (string.Equals(classD.Id, "fangsworn", System.StringComparison.OrdinalIgnoreCase))
        {
            c.FightingStyle = string.IsNullOrEmpty(FightingStyle) ? "duelist" : FightingStyle;
        }

        // 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;
    }

    // ── Phase 6.5 M4: Hybrid build path ─────────────────────────────────

    /// <summary>
    /// Validate the hybrid-origin fields. Returns true and an empty error
    /// string when the sire+dam configuration is valid for building a
    /// hybrid character.
    ///
    /// Required: both sire and dam picked (clade + species each); sire and
    /// dam must be *different* clades (cross-clade is the definition of
    /// hybrid); each species must belong to its declared clade.
    /// </summary>
    public bool ValidateHybrid(out string error)
    {
        error = "";
        if (HybridSireClade   is null) { error = "Sire clade not selected."; return false; }
        if (HybridSireSpecies is null) { error = "Sire species not selected."; return false; }
        if (HybridDamClade    is null) { error = "Dam clade not selected.";  return false; }
        if (HybridDamSpecies  is null) { error = "Dam species not selected.";  return false; }

        if (string.Equals(HybridSireClade.Id, HybridDamClade.Id, StringComparison.OrdinalIgnoreCase))
        {
            error = $"Sire and dam must be different clades (both are '{HybridSireClade.Id}'). Hybrids are cross-clade.";
            return false;
        }

        if (!string.Equals(HybridSireSpecies.CladeId, HybridSireClade.Id, StringComparison.OrdinalIgnoreCase))
        {
            error = $"Sire species '{HybridSireSpecies.Id}' belongs to clade '{HybridSireSpecies.CladeId}', not '{HybridSireClade.Id}'.";
            return false;
        }
        if (!string.Equals(HybridDamSpecies.CladeId, HybridDamClade.Id, StringComparison.OrdinalIgnoreCase))
        {
            error = $"Dam species '{HybridDamSpecies.Id}' belongs to clade '{HybridDamSpecies.CladeId}', not '{HybridDamClade.Id}'.";
            return false;
        }

        return true;
    }

    /// <summary>
    /// Build a hybrid character from the configured sire + dam pair. The
    /// builder resolves the dominant parent's clade + species as the
    /// primary <see cref="Character.Clade"/> / <see cref="Character.Species"/>
    /// (so existing systems that key off these fields keep working), and
    /// records the full sire+dam genealogy in <see cref="Character.Hybrid"/>.
    ///
    /// Ability mod blending follows <c>clades.md</c> HYBRID ORIGIN:
    /// take *one* ability mod from each parent clade. If both grant the
    /// same ability, the duplicate is dropped (no double-counting); the
    /// player picks an alternative +1 elsewhere via the standard array
    /// or roll path. (M4 simplification: take both clade mod sets and
    /// blend them — duplicates collapse to a single +1 — and use both
    /// species mods.)
    /// </summary>
    public bool TryBuildHybrid(
        IReadOnlyDictionary<string, ItemDef>? itemsForStartingKit,
        out Character? character,
        out string error)
    {
        character = null;

        if (!ValidateHybrid(out error)) return false;
        if (ClassDef is null)
        {
            error = "Class not selected.";
            return false;
        }
        if (Background is null)
        {
            error = "Background not selected.";
            return false;
        }

        // Validate skills against the class — same as the purebred path.
        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;
        }

        // Resolve the dominant lineage as the primary clade/species so the
        // rest of the engine (rendering, scent reads, dialogue gates that
        // key off Character.Clade / Character.Species) sees the dominant
        // expression. The Hybrid record carries the full sire+dam
        // genealogy.
        var dominantClade   = HybridDominantParent == ParentLineage.Sire
            ? HybridSireClade!  : HybridDamClade!;
        var dominantSpecies = HybridDominantParent == ParentLineage.Sire
            ? HybridSireSpecies! : HybridDamSpecies!;

        // Blend ability mods: take ONE chosen ability mod from each parent
        // clade — the picks are recorded on HybridSireChosenAbility /
        // HybridDamChosenAbility (set by the wizard's StepClade lineage
        // bonus picker, or left empty in headless tests for no bonus).
        // Picks stack additively if both parents land on the same ability.
        // Species mods don't apply for hybrids per project decision; the
        // 2-mod ceiling intentionally caps hybrid bonuses below purebred's.
        var modded = BaseAbilities;
        modded = ApplyOneMod(modded, HybridSireChosenAbility, HybridSireClade!.AbilityMods);
        modded = ApplyOneMod(modded, HybridDamChosenAbility,  HybridDamClade!.AbilityMods);

        var c = new Character(dominantClade, dominantSpecies, ClassDef, Background, modded)
        {
            Level = 1,
            Xp    = 0,
            Hybrid = new HybridState
            {
                SireClade      = HybridSireClade.Id,
                SireSpecies    = HybridSireSpecies.Id,
                DamClade       = HybridDamClade.Id,
                DamSpecies     = HybridDamSpecies.Id,
                DominantParent = HybridDominantParent,
            },
        };

        // Skills (same as purebred path).
        foreach (var s in ChosenClassSkills) c.SkillProficiencies.Add(s);
        foreach (var raw in Background.SkillProficiencies)
        {
            try { c.SkillProficiencies.Add(SkillIdExtensions.FromJson(raw)); }
            catch (ArgumentException) { /* unknown skill names ignored */ }
        }

        c.MaxHp     = c.ComputeMaxHpFromScratch();
        c.CurrentHp = c.MaxHp;

        if (string.Equals(ClassDef.Id, "fangsworn", System.StringComparison.OrdinalIgnoreCase))
            c.FightingStyle = string.IsNullOrEmpty(FightingStyle) ? "duelist" : FightingStyle;

        if (itemsForStartingKit is not null)
            ApplyStartingKit(c, itemsForStartingKit);

        character = c;
        return true;
    }

    /// <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);
    }

    /// <summary>
    /// Apply a single mod sourced from <paramref name="modsSource"/> at key
    /// <paramref name="chosenAbility"/>. No-op when the key is empty (player
    /// hasn't chosen) or absent from the source (defensive — the wizard's
    /// picker should only offer abilities present in the clade's mods, but
    /// we don't want a typo'd save to crash character finalize).
    /// </summary>
    private static AbilityScores ApplyOneMod(
        AbilityScores a, string chosenAbility, IReadOnlyDictionary<string, int> modsSource)
    {
        if (string.IsNullOrEmpty(chosenAbility)) return a;
        if (!TryParseAbility(chosenAbility, out var id)) return a;
        if (modsSource is null || !modsSource.TryGetValue(chosenAbility, out int value) || value == 0)
            return a;
        var dict = new Dictionary<AbilityId, int> { [id] = 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",

        SkillId.Brawl           => "brawl",
        SkillId.BuildRead       => "build_read",
        SkillId.Driving         => "driving",
        SkillId.Endurance       => "endurance",
        SkillId.Force           => "force",
        SkillId.Fortitude       => "fortitude",
        SkillId.Hardiness       => "hardiness",
        SkillId.Haulage         => "haulage",
        SkillId.LungCraft       => "lung_craft",
        SkillId.Marksmanship    => "marksmanship",
        SkillId.PainTolerance   => "pain_tolerance",
        SkillId.ScentSpeak      => "scent_speak",
        _ => 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;
    }
}
