using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Theriapolis.Core;
using Theriapolis.Core.Data;
using Theriapolis.Core.Rules.Character;
using Theriapolis.Core.Rules.Stats;
using Theriapolis.Core.Util;
using Theriapolis.Game.CodexUI.Core;
using Theriapolis.Game.CodexUI.Drag;
using Theriapolis.Game.CodexUI.Steps;
using Theriapolis.Game.CodexUI.Widgets;
using Theriapolis.Game.Screens;
using Theriapolis.Game.UI;

namespace Theriapolis.Game.CodexUI.Screens;

/// <summary>
/// Custom-rendered character-creation wizard. State model is a verbatim port
/// of <see cref="CharacterCreationScreen"/> (the Myra version); only the
/// rendering swaps to CodexUI primitives. Layout: codex header, 7-step
/// stepper, two-column body (page main + aside summary), nav bar at the
/// bottom. Each step is a separate file under <c>Steps/</c> that builds
/// its own widget subtree against this screen's mutable state.
/// </summary>
public sealed class CodexCharacterCreationScreen : CodexScreen
{
    public ulong Seed { get; }

    // Loaded content
    public ContentResolver Content { get; private set; } = null!;
    public CladeDef[]      Clades      { get; private set; } = null!;
    public SpeciesDef[]    AllSpecies  { get; private set; } = null!;
    public ClassDef[]      Classes     { get; private set; } = null!;
    public BackgroundDef[] Backgrounds { get; private set; } = null!;

    // Wizard state
    public int Step;
    public CladeDef?      Clade;
    public SpeciesDef?    Species;
    public ClassDef?      Class;
    public BackgroundDef? Background;
    public string Name = "Wanderer";

    // Stat assignment state
    public bool UseRoll;
    public readonly System.Collections.Generic.List<int> StatPool = new();
    public readonly System.Collections.Generic.Dictionary<AbilityId, int> StatAssign = new();
    public readonly System.Collections.Generic.List<int[]> StatHistory = new();
    public int? PendingPoolIdx;

    // Skill state
    public readonly System.Collections.Generic.HashSet<SkillId> ChosenSkills = new();

    // Scroll position for the body's scroll panel. Survives any
    // InvalidateLayout-triggered rebuild within the same step (so
    // selecting a clade or dropping an ability die doesn't bounce the
    // page back to the top); reset to zero on step change.
    private int _bodyScrollOffset;

    // Same idea for the right-column aside summary, which can grow taller
    // than the viewport once enough trait/feature/skill chips appear.
    // Persisted across rebuilds; not reset on step change because the
    // aside content keeps growing as more folios are completed.
    private int _asideScrollOffset;

    /// <summary>
    /// Popover layer for hover trigger widgets in the right-column aside.
    /// Exposed so <see cref="CodexAside"/> can attach hover triggers to
    /// the same parchment-and-gilt popover the page-main cards use.
    /// </summary>
    public CodexHoverPopover AsidePopover => Popover ?? throw new System.InvalidOperationException("AsidePopover accessed before BuildRoot.");

    // Stat-roll seeding (Phase 5 plan §4.2)
    private readonly long _gameStartMs;
    private long _msAtScreenOpen;

    public static readonly string[] StepNames = new[]
    {
        "Clade", "Species", "Calling", "History", "Abilities", "Skills", "Sign",
    };

    public CodexCharacterCreationScreen(ulong seed)
    {
        Seed = seed;
        _gameStartMs = System.Environment.TickCount64;
    }

    public override void Initialize(Game1 game)
    {
        var loader = new ContentLoader(game.ContentDataDirectory);
        Content      = new ContentResolver(loader);
        Clades       = Content.Clades.Values.OrderBy(c => c.Id).ToArray();
        AllSpecies   = Content.Species.Values.OrderBy(s => s.Id).ToArray();
        Classes      = Content.Classes.Values.OrderBy(c => c.Id).ToArray();
        Backgrounds  = Content.Backgrounds.Values.OrderBy(b => b.Id).ToArray();
        _msAtScreenOpen = System.Environment.TickCount64 - _gameStartMs;

        // No pre-filled clade / species / class / background. Defaults
        // would let the player jump straight to the review step without
        // ever interacting with the earlier folios; explicit selection
        // gates each step via ValidateStep + the stepper's lock logic.
        // Stat pool is initialised so the abilities folio has values to
        // drag from once the player reaches it.
        InitStandardArrayPool();

        DragDrop.OnDrop += HandleDrop;
        DragDrop.OnDropAnywhere += HandleDropAnywhere;
        DragDrop.OnCancel += _ => { };

        base.Initialize(game);
    }

    // ── Layout ───────────────────────────────────────────────────────────

    protected override CodexWidget BuildRoot()
    {
        Popover ??= new CodexHoverPopover(Atlas);
        Popover.UpdateViewport(Game.GraphicsDevice.Viewport.Bounds);

        // Header
        var headerRow = new Row { Spacing = 16, VAlignChildren = VAlign.Bottom, Padding = new Thickness(36, 22, 36, 18) };
        headerRow.Add(new CodexLabel("THERIAPOLIS — Codex of Becoming", CodexFonts.DisplayLarge, CodexColors.Ink));
        headerRow.Add(new CodexLabel($"FOLIO {Romanize(Step + 1)} OF VII · SEED 0x{Seed:X}",
                                     CodexFonts.MonoTagSmall, CodexColors.InkMute));

        // Stepper — locked steps are anything beyond the first incomplete folio.
        // We deliberately ignore the player's current `Step` here: the lock
        // depends only on whether earlier folios are valid. Pre-filled defaults
        // would make every step pass validation without interaction; removing
        // them in Initialize is what makes this gate actually gate.
        var stepper = new CodexStepper(StepNames, Atlas) { Current = Step };
        int firstIncomplete = -1;
        for (int j = 0; j < StepNames.Length; j++)
            if (ValidateStep(j) is not null) { firstIncomplete = j; break; }
        for (int i = 0; i < StepNames.Length; i++)
        {
            stepper.Complete[i] = ValidateStep(i) is null && i != Step;
            stepper.Locked[i]   = firstIncomplete != -1 && firstIncomplete < i;
        }
        stepper.OnPick = NavigateTo;

        // Body — two-column layout, both columns are independently
        // scrollable. The body offsets are restored from saved state so
        // an interaction that triggers InvalidateLayout (selecting a
        // clade, dropping a die into an ability slot) doesn't bounce
        // the user to the top of either column.
        //
        // Bottom padding is zero on purpose: the ScrollPanel's mouse clip
        // matches its own bounds, so any column-padding gap below the
        // panel becomes a region where chips can render visibly (no
        // scissor) but reject hover (cursor outside the clip). With the
        // panel reaching all the way down to the body's bottom edge —
        // which sits exactly atop the nav bar's hairline rule — chips
        // that scroll past it disappear under the nav bar's opaque mask
        // instead of leaking into a padding strip.
        var body = new TwoColumn(Atlas) { LeftPad = new Thickness(36, 28, 36, 0), RightPad = new Thickness(28, 28, 28, 0) };

        var leftScroll = new ScrollPanel(Atlas, BuildCurrentStep());
        leftScroll.SetInitialScroll(_bodyScrollOffset);
        leftScroll.OnScrollChanged = o => _bodyScrollOffset = o;
        body.Left = leftScroll;

        var rightScroll = new ScrollPanel(Atlas, new CodexAside(this, Atlas).Build());
        rightScroll.SetInitialScroll(_asideScrollOffset);
        rightScroll.OnScrollChanged = o => _asideScrollOffset = o;
        body.Right = rightScroll;

        // Nav bar
        var navBar = BuildNavBar();

        // Wrap everything in a custom root that gives the body whatever
        // height is left after header + stepper + nav. Without this the
        // body's measured height was its full content height (often >2×
        // the viewport), so cards in row 2/3 sat below the visible window.
        return new CodexRootLayout(Atlas, headerRow, stepper, body, navBar);
    }

    private CodexWidget BuildCurrentStep() => Step switch
    {
        0 => StepClade.Build(this, Atlas, Popover!),
        1 => StepSpecies.Build(this, Atlas, Popover!),
        2 => StepClass.Build(this, Atlas, Popover!),
        3 => StepBackground.Build(this, Atlas, Popover!),
        4 => StepStats.Build(this, Atlas, Popover!, DragDrop),
        5 => StepSkills.Build(this, Atlas, Popover!),
        6 => StepReview.Build(this, Atlas, Popover!),
        _ => new CodexLabel("(unknown step)", CodexFonts.SerifBody),
    };

    private CodexWidget BuildNavBar()
    {
        var row = new Row
        {
            Spacing = 16,
            Padding = new Thickness(36, 16, 36, 16),
            VAlignChildren = VAlign.Middle,
        };

        var back = new CodexButton("‹ Back", Atlas, CodexButtonVariant.Ghost,
                                   onClick: () => NavigateTo(System.Math.Max(0, Step - 1)),
                                   fixedWidth: 120);
        back.Enabled = Step > 0;
        row.Add(back);

        // Status label
        var stepError = ValidateStep(Step);
        bool allValid = AllStepsValid();
        string status = stepError is not null
            ? "✘ " + stepError
            : (Step < 6 ? "Folio complete" : (allValid ? "Ready to sign" : "Some folios remain"));
        Color statusColor = stepError is not null ? CodexColors.Seal : CodexColors.InkMute;
        var statusLabel = new CodexLabel(status, CodexFonts.MonoTag, statusColor) { HAlign = HAlign.Center };
        // Make the label expand by wrapping in a stretched Padding
        var spacer = new Padding(statusLabel, new Thickness(60, 0, 60, 0));
        row.Add(spacer);

        if (Step < StepNames.Length - 1)
        {
            var next = new CodexButton("Next ›", Atlas, CodexButtonVariant.Ghost,
                                       onClick: () => NavigateTo(Step + 1), fixedWidth: 140);
            next.Enabled = stepError is null;
            row.Add(next);
        }
        else
        {
            var confirm = new CodexButton("Confirm & Begin", Atlas, CodexButtonVariant.Primary,
                                          onClick: OnConfirm, fixedWidth: 220);
            confirm.Enabled = allValid;
            row.Add(confirm);
        }
        return row;
    }

    private void NavigateTo(int s)
    {
        Step = s;
        _bodyScrollOffset = 0;  // each folio starts at the top
        InvalidateLayout();
    }

    public override void Update(GameTime gameTime)
    {
        base.Update(gameTime);
        if (Input.KeyJustPressed(Keys.Escape)) Game.Screens.Pop();
    }

    // ── State helpers ────────────────────────────────────────────────────

    public void InitStandardArrayPool()
    {
        StatPool.Clear();
        foreach (int v in AbilityScores.StandardArray) StatPool.Add(v);
        StatAssign.Clear();
        PendingPoolIdx = null;
    }

    public void RollAndPool()
    {
        ulong msNow = (ulong)(System.Environment.TickCount64 - _gameStartMs);
        var rng = SeededRng.ForSubsystem(Seed, C.RNG_STAT_ROLL ^ msNow);
        var vals = new int[6];
        for (int i = 0; i < 6; i++) vals[i] = CharacterBuilder.Roll4d6DropLowest(rng);
        StatHistory.Add(vals);
        StatPool.Clear();
        foreach (var v in vals) StatPool.Add(v);
        StatAssign.Clear();
        PendingPoolIdx = null;
    }

    public void AutoAssignByClassPriority()
    {
        var primary = Class?.PrimaryAbility ?? System.Array.Empty<string>();
        var order = new System.Collections.Generic.List<string>();
        foreach (var p in primary) order.Add(p.ToUpperInvariant());
        foreach (var a in new[] { "CON", "DEX", "STR", "WIS", "INT", "CHA" })
            if (!order.Contains(a)) order.Add(a);

        var available = StatPool.OrderByDescending(x => x).ToList();
        var emptyAbilities = new System.Collections.Generic.List<AbilityId>();
        foreach (var s in order)
            if (TryParseAbility(s, out var ab) && !StatAssign.ContainsKey(ab))
                emptyAbilities.Add(ab);

        for (int i = 0; i < emptyAbilities.Count && i < available.Count; i++)
            StatAssign[emptyAbilities[i]] = available[i];

        StatPool.Clear();
        for (int i = emptyAbilities.Count; i < available.Count; i++) StatPool.Add(available[i]);
        PendingPoolIdx = null;
    }

    public void ClearAssignments()
    {
        foreach (var v in StatAssign.Values) StatPool.Add(v);
        StatAssign.Clear();
        PendingPoolIdx = null;
    }

    private void HandleDrop(object payload, string targetId)
    {
        if (payload is not StatPoolPayload p) return;
        if (targetId.StartsWith("ability:"))
        {
            string abStr = targetId.Substring("ability:".Length);
            if (!System.Enum.TryParse<AbilityId>(abStr, out var dest)) return;
            if (p.Source == "pool" && p.PoolIdx is int idx && idx < StatPool.Count)
            {
                if (StatAssign.TryGetValue(dest, out var existing))
                    StatPool.Add(existing);
                StatPool.RemoveAt(idx);
                StatAssign[dest] = p.Value;
            }
            else if (p.Source == "slot" && p.Ability is AbilityId src)
            {
                if (src == dest) return;
                int srcVal = p.Value;
                if (StatAssign.TryGetValue(dest, out var destVal))
                {
                    StatAssign[dest] = srcVal;
                    StatAssign[src] = destVal;
                }
                else
                {
                    StatAssign[dest] = srcVal;
                    StatAssign.Remove(src);
                }
            }
            InvalidateLayout();
        }
        else if (targetId == "pool")
        {
            if (p.Source == "slot" && p.Ability is AbilityId src && StatAssign.TryGetValue(src, out var v))
            {
                StatPool.Add(v);
                StatAssign.Remove(src);
                InvalidateLayout();
            }
        }
    }

    private void HandleDropAnywhere(object payload, Point screenPos)
    {
        // No-op — payload silently bounces back when dropped outside any registered target.
    }

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

    public string? ValidateStep(int i)
    {
        if (i == 0) return Clade is null ? "Pick a clade." : null;
        if (i == 1) return Species is null ? "Pick a species." : null;
        if (i == 2) return Class is null ? "Pick a calling." : null;
        if (i == 3) return Background is null ? "Pick a background." : null;
        if (i == 4) return StatAssign.Count == 6 ? null : $"Assign all six abilities ({StatAssign.Count}/6).";
        if (i == 5)
        {
            int need = Class?.SkillsChoose ?? 0;
            return ChosenSkills.Count == need ? null : $"Pick exactly {need} skill(s) ({ChosenSkills.Count}/{need}).";
        }
        if (i == 6) return string.IsNullOrWhiteSpace(Name) ? "Enter a name." : null;
        return null;
    }

    public bool AllStepsValid()
    {
        for (int i = 0; i < StepNames.Length; i++) if (ValidateStep(i) is not null) return false;
        return true;
    }

    private void OnConfirm()
    {
        if (!AllStepsValid()) return;
        var b = new CharacterBuilder
        {
            Clade        = Clade,
            Species      = Species,
            ClassDef     = Class,
            Background   = Background,
            BaseAbilities = new AbilityScores(
                StatAssign.GetValueOrDefault(AbilityId.STR),
                StatAssign.GetValueOrDefault(AbilityId.DEX),
                StatAssign.GetValueOrDefault(AbilityId.CON),
                StatAssign.GetValueOrDefault(AbilityId.INT),
                StatAssign.GetValueOrDefault(AbilityId.WIS),
                StatAssign.GetValueOrDefault(AbilityId.CHA)),
            Name = Name,
        };
        foreach (var s in ChosenSkills) b.ChooseSkill(s);
        var character = b.Build(Content.Items);

        Game.Screens.Pop();
        Game.Screens.Push(new WorldGenProgressScreen(Seed, pendingCharacter: character, pendingName: Name));
    }

    // Helpers
    public int CladeMod(AbilityId ab)   => ModFromDict(Clade?.AbilityMods,   ab);
    public int SpeciesMod(AbilityId ab) => ModFromDict(Species?.AbilityMods, ab);
    public int TotalBonus(AbilityId ab) => CladeMod(ab) + SpeciesMod(ab);
    public bool IsPrimary(AbilityId ab) => Class?.PrimaryAbility.Contains(ab.ToString()) == true;

    public static int ModFromDict(System.Collections.Generic.IReadOnlyDictionary<string, int>? d, AbilityId ab)
    {
        if (d is null) return 0;
        return d.TryGetValue(ab.ToString(), out var v) ? v : 0;
    }

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

    public static System.Collections.Generic.IEnumerable<string> AllSkillIds() => new[]
    {
        "athletics", "acrobatics", "sleight_of_hand", "stealth",
        "arcana", "history", "investigation", "nature", "religion",
        "animal_handling", "insight", "medicine", "perception", "survival",
        "deception", "intimidation", "performance", "persuasion",
    };

    public static string Romanize(int n) => CodexCopy.Romanize(n);
}

/// <summary>
/// Two-column body layout: page-main (flex) on the left, fixed-width aside
/// panel on the right. Mirrors the React design's <c>.page</c> grid. Takes
/// whatever height the parent assigns (so the page-main scroll panel
/// scrolls within a viewport-bounded region) rather than expanding to its
/// content's natural height.
/// </summary>
internal sealed class TwoColumn : CodexWidget
{
    public CodexWidget? Left;
    public CodexWidget? Right;
    public Thickness LeftPad;
    public Thickness RightPad;
    private readonly CodexAtlas _atlas;
    public TwoColumn(CodexAtlas atlas) { _atlas = atlas; }

    protected override Point MeasureCore(Point available)
    {
        int rightW = CodexDensity.AsideWidth;
        int leftW  = available.X - rightW;
        Left?.Measure(new Point(leftW  - LeftPad.HorizontalSum(),  available.Y - LeftPad.VerticalSum()));
        Right?.Measure(new Point(rightW - RightPad.HorizontalSum(), available.Y - RightPad.VerticalSum()));
        return new Point(available.X, available.Y);
    }

    protected override void ArrangeCore(Rectangle bounds)
    {
        int rightW = CodexDensity.AsideWidth;
        int leftW  = bounds.Width - rightW;
        Left?.Arrange(new Rectangle(
            bounds.X + LeftPad.Left,
            bounds.Y + LeftPad.Top,
            leftW - LeftPad.HorizontalSum(),
            bounds.Height - LeftPad.VerticalSum()));
        Right?.Arrange(new Rectangle(
            bounds.X + leftW + RightPad.Left,
            bounds.Y + RightPad.Top,
            rightW - RightPad.HorizontalSum(),
            bounds.Height - RightPad.VerticalSum()));
    }

    public override void Update(GameTime gt, CodexInput input)
    {
        Left?.Update(gt, input);
        Right?.Update(gt, input);
    }

    public override void Draw(SpriteBatch sb, GameTime gt)
    {
        // Body fill — paint the lighter parchment Bg behind both columns
        // so cards (Bg2) sit on a flat surface with clear contrast,
        // independent of the screen's BgDeep clear.
        sb.Draw(_atlas.Pixel, Bounds, CodexColors.Bg);

        // Vertical rule between left and right
        int rightW = CodexDensity.AsideWidth;
        int leftW = Bounds.Width - rightW;
        sb.Draw(_atlas.Pixel, new Rectangle(Bounds.X + leftW, Bounds.Y, 1, Bounds.Height), CodexColors.Rule);

        Left?.Draw(sb, gt);
        Right?.Draw(sb, gt);
    }
}

internal sealed class RuleLine : CodexWidget
{
    private readonly CodexAtlas _atlas;
    public RuleLine(CodexAtlas atlas) { _atlas = atlas; }
    protected override Point MeasureCore(Point available) => new(available.X, 1);
    protected override void ArrangeCore(Rectangle bounds) { }
    public override void Draw(SpriteBatch sb, GameTime gt) => sb.Draw(_atlas.Pixel, Bounds, CodexColors.Rule);
}

/// <summary>
/// Wizard-shaped root layout: header at the top, stepper under it, body
/// fills the middle, nav bar at the bottom. Header / stepper / nav take
/// their measured size; body gets whatever height is left over so it
/// always fits the viewport (instead of overflowing when its content is
/// taller than the window). The stepper and nav bar paint opaque
/// parchment backgrounds that mask any scroll-panel overflow above /
/// below the body's clipped region — cheaper than configuring a scissor-
/// enabled rasterizer and re-Begin-ing the SpriteBatch.
/// </summary>
internal sealed class CodexRootLayout : CodexWidget
{
    private readonly CodexAtlas _atlas;
    public CodexWidget Header;
    public CodexWidget Stepper;
    public CodexWidget Body;
    public CodexWidget NavBar;

    public CodexRootLayout(CodexAtlas atlas, CodexWidget header, CodexWidget stepper, CodexWidget body, CodexWidget navBar)
    {
        _atlas   = atlas;
        Header   = header;
        Stepper  = stepper;
        Body     = body;
        NavBar   = navBar;
        Header.Parent = this; Stepper.Parent = this; Body.Parent = this; NavBar.Parent = this;
    }

    protected override Point MeasureCore(Point available)
    {
        var hs = Header.Measure(available);
        var ss = Stepper.Measure(available);
        var ns = NavBar.Measure(available);
        int bodyH = System.Math.Max(0, available.Y - hs.Y - ss.Y - ns.Y - 2);  // 2 = top/bottom rules
        Body.Measure(new Point(available.X, bodyH));
        return new Point(available.X, available.Y);
    }

    protected override void ArrangeCore(Rectangle bounds)
    {
        int y = bounds.Y;
        Header.Arrange(new Rectangle(bounds.X, y, bounds.Width, Header.DesiredSize.Y));
        y += Header.DesiredSize.Y;
        // Header bottom rule
        y += 1;
        Stepper.Arrange(new Rectangle(bounds.X, y, bounds.Width, Stepper.DesiredSize.Y));
        y += Stepper.DesiredSize.Y;
        int navY = bounds.Bottom - NavBar.DesiredSize.Y;
        Body.Arrange(new Rectangle(bounds.X, y, bounds.Width, navY - y - 1));
        // Nav top rule on (navY - 1).
        NavBar.Arrange(new Rectangle(bounds.X, navY, bounds.Width, NavBar.DesiredSize.Y));
    }

    public override void Update(GameTime gt, CodexInput input)
    {
        // Clip the body's mouse hit-testing to its own rectangle so cards
        // scrolled under the stepper / nav bar don't receive clicks that
        // visually land on the chrome above or below them. Without this,
        // clicking a stepper bullet would also fire the OnClick of any
        // card whose bounds (at their scroll-offset position) happen to
        // intersect the cursor — even though the card is masked from view.
        input.SetMouseClip(Body.Bounds);
        Body.Update(gt, input);
        input.ClearMouseClip();

        Header.Update(gt, input);
        Stepper.Update(gt, input);
        NavBar.Update(gt, input);
    }

    public override void Draw(SpriteBatch sb, GameTime gt)
    {
        // Body draws first; chrome paints over any scroll overflow.
        Body.Draw(sb, gt);

        // Header: opaque parchment band + bottom hairline rule + text.
        var headerRect = new Rectangle(Header.Bounds.X, Header.Bounds.Y, Header.Bounds.Width, Header.Bounds.Height);
        sb.Draw(_atlas.Pixel, headerRect, CodexColors.Bg);
        sb.Draw(_atlas.Pixel, new Rectangle(headerRect.X, headerRect.Bottom, headerRect.Width, 1), CodexColors.Rule);
        Header.Draw(sb, gt);

        // Stepper paints its own opaque parchment background.
        Stepper.Draw(sb, gt);

        // Nav bar: opaque parchment band + top hairline rule + buttons.
        sb.Draw(_atlas.Pixel, new Rectangle(NavBar.Bounds.X, NavBar.Bounds.Y - 1, NavBar.Bounds.Width, 1), CodexColors.Rule);
        sb.Draw(_atlas.Pixel, NavBar.Bounds, CodexColors.Bg);
        NavBar.Draw(sb, gt);
    }
}
