using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using Godot;
using Theriapolis.Core.Data;
using Theriapolis.Core.Items;
using Theriapolis.Core.Persistence;
using Theriapolis.Core.Rules.Character;
using Theriapolis.Core.Rules.Stats;
using Theriapolis.GodotHost.Platform;

namespace Theriapolis.GodotHost.UI;

/// <summary>
/// Bridges the wizard's <see cref="CharacterDraft"/> (Godot Resource) into a
/// runtime <see cref="Character"/> via <see cref="CharacterBuilder"/>. Used by
/// StepReview's "Confirm &amp; Begin" handoff.
///
/// Holds the most-recently-built character in <see cref="LastBuilt"/> so a
/// future PlayScreen / world-load step can pick it up without re-running the
/// builder, and writes the captured <see cref="PlayerCharacterState"/> to
/// <c>user://character.json</c> for cold-restart resumability before the M7
/// save format lands.
/// </summary>
public static class CharacterAssembler
{
    /// <summary>The most recently confirmed character, or null if none.</summary>
    public static Character? LastBuilt { get; private set; }

    /// <summary>Path used for the resumability dump.</summary>
    public const string PersistedStatePath = "user://character.json";

    private static IReadOnlyDictionary<string, ItemDef>? _items;

    private static IReadOnlyDictionary<string, ItemDef> Items()
    {
        if (_items is not null) return _items;
        var loader = new Theriapolis.Core.Data.ContentLoader(ContentPaths.DataDir);
        var arr = loader.LoadItems();
        var dict = new Dictionary<string, ItemDef>(arr.Length);
        foreach (var it in arr) dict[it.Id] = it;
        _items = dict;
        return _items;
    }

    /// <summary>
    /// Build the runtime <see cref="Character"/> from the draft. Returns false
    /// with a populated <paramref name="error"/> when the draft is incomplete
    /// or content lookups fail; on success <paramref name="character"/> holds
    /// the built object and <see cref="LastBuilt"/> is updated.
    /// </summary>
    public static bool TryBuild(CharacterDraft draft, out Character? character, out string error)
    {
        character = null;
        error = "";

        var builder = new CharacterBuilder
        {
            BaseAbilities = ToAbilityScores(draft.StatAssign),
            Name          = string.IsNullOrWhiteSpace(draft.CharacterName) ? "Wanderer" : draft.CharacterName,
        };

        // Class + background lookups are shared by purebred and hybrid paths.
        var classDef = CodexContent.Class(draft.ClassId);
        if (classDef is null) { error = $"Unknown class id '{draft.ClassId}'."; return false; }
        builder.ClassDef = classDef;

        var bgDef = CodexContent.Background(draft.BackgroundId);
        if (bgDef is null) { error = $"Unknown background id '{draft.BackgroundId}'."; return false; }
        builder.Background = bgDef;

        foreach (Variant v in draft.ChosenSkills)
        {
            string raw = (string)v;
            try { builder.ChosenClassSkills.Add(SkillIdExtensions.FromJson(raw)); }
            catch (System.ArgumentException) { error = $"Unknown skill id '{raw}'."; return false; }
        }

        Character built;
        if (draft.IsHybrid)
        {
            builder.IsHybridOrigin = true;

            var sireClade = CodexContent.Clade(draft.SireCladeId);
            var sireSp    = CodexContent.SpeciesById(draft.SireSpeciesId);
            var damClade  = CodexContent.Clade(draft.DamCladeId);
            var damSp     = CodexContent.SpeciesById(draft.DamSpeciesId);
            if (sireClade is null || sireSp is null || damClade is null || damSp is null)
            {
                error = "Hybrid lineage incomplete (sire/dam clade or species missing).";
                return false;
            }

            builder.HybridSireClade   = sireClade;
            builder.HybridSireSpecies = sireSp;
            builder.HybridDamClade    = damClade;
            builder.HybridDamSpecies  = damSp;
            builder.HybridDominantParent =
                draft.DominantParent == "dam" ? ParentLineage.Dam : ParentLineage.Sire;
            // Hybrid lineage-bonus picks: the wizard's StepClade picker
            // records one chosen ability per parent. The builder applies
            // exactly those (no full ability_mods spread, no species mods)
            // so the built Character matches the Aside's preview math.
            builder.HybridSireChosenAbility = draft.SireChosenAbility ?? "";
            builder.HybridDamChosenAbility  = draft.DamChosenAbility  ?? "";

            if (!builder.TryBuildHybrid(Items(), out var hybridChar, out error)) return false;
            built = hybridChar!;
        }
        else
        {
            var clade = CodexContent.Clade(draft.CladeId);
            var sp    = CodexContent.SpeciesById(draft.SpeciesId);
            if (clade is null || sp is null)
            {
                error = "Lineage incomplete (clade or species missing).";
                return false;
            }
            builder.Clade   = clade;
            builder.Species = sp;

            try { built = builder.Build(Items()); }
            catch (System.InvalidOperationException ex) { error = ex.Message; return false; }
        }

        // Subclass selection happens at level 3 mechanically, but the wizard
        // locks the choice in at L1 — stamp it onto the character so the L3
        // unlock has the player's pick already recorded.
        if (!string.IsNullOrEmpty(draft.SubclassId)) built.SubclassId = draft.SubclassId;

        character = built;
        LastBuilt = built;
        PersistState(built);
        return true;
    }

    /// <summary>
    /// Capture the live <see cref="Character"/> into a flat
    /// <see cref="PlayerCharacterState"/> and write it as JSON to
    /// <see cref="PersistedStatePath"/>. Lets a future cold-load path
    /// recover the confirmed character before the M7 save format lands.
    /// </summary>
    private static void PersistState(Character c)
    {
        try
        {
            var snap = CharacterCodec.Capture(c);
            string globalPath = ProjectSettings.GlobalizePath(PersistedStatePath);
            string? dir = Path.GetDirectoryName(globalPath);
            if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
            string json = JsonSerializer.Serialize(snap, new JsonSerializerOptions { WriteIndented = true });
            File.WriteAllText(globalPath, json);
            GD.Print($"[character] Persisted state to {PersistedStatePath}");
        }
        catch (System.Exception ex)
        {
            GD.PushWarning($"[character] PersistState failed: {ex.Message}");
        }
    }

    private static AbilityScores ToAbilityScores(Godot.Collections.Dictionary statAssign)
    {
        int Get(string key) => statAssign.ContainsKey(key) ? (int)statAssign[key] : 10;
        return new AbilityScores(
            Get("STR"), Get("DEX"), Get("CON"),
            Get("INT"), Get("WIS"), Get("CHA"));
    }
}
