using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Myra.Graphics2D;
using Myra.Graphics2D.Brushes;
using Myra.Graphics2D.UI;
using Theriapolis.Core.Data;
using Theriapolis.Core.Entities;
using Theriapolis.Core.Rules.Dialogue;
using Theriapolis.Core.Rules.Reputation;

namespace Theriapolis.Game.Screens;

/// <summary>
/// Phase 6 M3 — full dialogue UI driven by <see cref="DialogueRunner"/>.
/// Pushed by <see cref="PlayScreen"/> when the player presses F next to a
/// friendly/neutral NPC.
///
/// Layout:
///   - Speaker header: NPC name + role + bias profile + effective disposition
///   - Scrollback: history of NPC lines, PC choices, narration (skill-check rolls)
///   - Options: numbered, conditions evaluated each refresh
///   - Footer: "(1-9 to choose · Esc to leave)"
///
/// On <c>open_shop</c> effect: pushes <see cref="ShopScreen"/>; resumes
/// dialogue when shop closes.
/// </summary>
public sealed class InteractionScreen : IScreen
{
    private readonly NpcActor          _npc;
    private readonly ContentResolver?  _content;
    private readonly PlayScreen?       _playScreen;
    private DialogueRunner?            _runner;

    private Game1 _game = null!;
    private Desktop _desktop = null!;
    private VerticalStackPanel _root = null!;
    private VerticalStackPanel _historyPanel = null!;
    private VerticalStackPanel _optionsPanel = null!;
    private bool _escWasDown   = true;
    private bool _fWasDown     = true;
    private bool _enterWasDown = true;
    private readonly bool[] _numWasDown = new bool[10];

    public InteractionScreen(NpcActor npc, ContentResolver? content = null, PlayScreen? playScreen = null)
    {
        _npc        = npc ?? throw new System.ArgumentNullException(nameof(npc));
        _content    = content;
        _playScreen = playScreen;
    }

    public void Initialize(Game1 game)
    {
        _game = game;
        _runner = TryBuildRunner();
        BuildLayout();
    }

    private DialogueRunner? TryBuildRunner()
    {
        if (_content is null || _playScreen is null) return null;
        var pc = _playScreen.PlayerCharacter();
        if (pc is null) return null;
        if (string.IsNullOrEmpty(_npc.DialogueId)) return null;
        if (!_content.Dialogues.TryGetValue(_npc.DialogueId, out var tree)) return null;

        var ctx = new DialogueContext(_npc, pc, _playScreen.Reputation, _playScreen.Flags, _content)
        {
            PlayerWorldTileX  = (int)(_playScreen.PlayerActorPosition().X / Theriapolis.Core.C.WORLD_TILE_PIXELS),
            PlayerWorldTileY  = (int)(_playScreen.PlayerActorPosition().Y / Theriapolis.Core.C.WORLD_TILE_PIXELS),
            WorldClockSeconds = _playScreen.ClockSeconds(),
        };
        return new DialogueRunner(tree, ctx, _playScreen.WorldSeed());
    }

    private void BuildLayout()
    {
        _root = new VerticalStackPanel
        {
            Spacing             = 8,
            HorizontalAlignment = HorizontalAlignment.Center,
            VerticalAlignment   = VerticalAlignment.Center,
            Padding             = new Thickness(40, 24, 40, 24),
            Background          = new SolidBrush(new Color(15, 12, 8, 235)),
            Width               = 760,
        };

        _root.Widgets.Add(BuildHeader());
        _historyPanel = new VerticalStackPanel { Spacing = 2, Width = 680 };
        _root.Widgets.Add(_historyPanel);
        _optionsPanel = new VerticalStackPanel { Spacing = 4, HorizontalAlignment = HorizontalAlignment.Center };
        _root.Widgets.Add(_optionsPanel);
        _root.Widgets.Add(new Label
        {
            Text                = "(1-9 to choose · Esc to leave · F also closes)",
            HorizontalAlignment = HorizontalAlignment.Center,
            TextColor           = new Color(120, 110, 100),
        });

        Refresh();
        _desktop = new Desktop { Root = _root };
    }

    private Widget BuildHeader()
    {
        var header = new VerticalStackPanel { Spacing = 2, HorizontalAlignment = HorizontalAlignment.Center };
        header.Widgets.Add(new Label
        {
            Text                = _npc.DisplayName,
            HorizontalAlignment = HorizontalAlignment.Center,
            TextColor           = new Color(255, 230, 170),
        });
        string roleLine = FormatRoleLine(_npc.RoleTag);
        if (!string.IsNullOrEmpty(roleLine))
            header.Widgets.Add(new Label { Text = roleLine, HorizontalAlignment = HorizontalAlignment.Center, TextColor = new Color(180, 160, 130) });

        // Disposition footnote: profile + score + label.
        if (_content is not null && _playScreen?.PlayerCharacter() is { } pc)
        {
            var br = EffectiveDisposition.Breakdown(_npc, pc, _playScreen.Reputation, _content,
                _playScreen.World(), _playScreen.WorldSeed());
            string profile = _content.BiasProfiles.TryGetValue(_npc.BiasProfileId, out var bp) ? bp.Name : _npc.BiasProfileId;
            header.Widgets.Add(new Label
            {
                Text                = $"[{profile}] · {DispositionLabels.DisplayName(br.Label)} {br.Total:+#;-#;0}",
                HorizontalAlignment = HorizontalAlignment.Center,
                TextColor           = new Color(120, 140, 180),
            });

            // Phase 6.5 M1 — Scent Literacy overlay. Appears when the PC has
            // the level-1 Scent-Broker feature; surfaces NPC clade, species,
            // and HP%. ScentTags (Phase 6.5 M6) appear here when authored.
            string? scentLine = ScentReadingFor(_npc, pc);
            if (scentLine is not null)
            {
                header.Widgets.Add(new Label
                {
                    Text                = scentLine,
                    HorizontalAlignment = HorizontalAlignment.Center,
                    TextColor           = new Color(180, 160, 200),
                });
            }
        }
        header.Widgets.Add(new Label { Text = " " });
        return header;
    }

    /// <summary>
    /// Phase 6.5 M1 — produce the Scent Literacy line for the dialogue
    /// header, or null if the PC doesn't have the feature. Scent Literacy
    /// is granted by the level-1 Scent-Broker entry in classes.json
    /// (<c>scent_literacy</c>) and tracked in
    /// <see cref="Theriapolis.Core.Rules.Character.Character.LearnedFeatureIds"/>
    /// after Phase 6.5 M0; for Phase-5-built characters that predate the
    /// LearnedFeatureIds wiring, fall back to a class-id check.
    ///
    /// Phase 6.5 M6 — surfaces the top <see cref="Theriapolis.Core.Entities.ScentTag"/>
    /// from <see cref="NpcActor.ComputeScentTags"/>. Scent Mastery
    /// (<c>master_nose</c>, level 11) reads up to 3 tags; baseline Scent
    /// Literacy reads the top 1.
    /// </summary>
    private static string? ScentReadingFor(NpcActor npc, Theriapolis.Core.Rules.Character.Character pc)
    {
        bool hasFeature = pc.LearnedFeatureIds.Contains("scent_literacy")
                       || pc.ClassDef.Id == "scent_broker";  // L1-default fallback for legacy saves
        if (!hasFeature) return null;

        string clade   = npc.Resident?.Clade ?? npc.Template?.Behavior ?? "unknown";
        string species = npc.Resident?.Species ?? "—";
        // HP% from the live actor.
        int hpPct = npc.MaxHp > 0
            ? (int)System.Math.Round(100.0 * npc.CurrentHp / npc.MaxHp)
            : 100;
        // Hide noise: NPCs we haven't damaged yet show "—" instead of 100% to
        // avoid "the innkeeper is at 100% HP" redundancy in flavour reads.
        string hp = hpPct == 100 ? "—" : $"{hpPct}%";

        // Phase 6.5 M6 — Scent Mastery (master_nose) reads up to 3 tags;
        // baseline Scent Literacy reads the top 1.
        int tagCount = pc.LearnedFeatureIds.Contains("master_nose") ? 3 : 1;
        var tags = npc.ComputeScentTags(tagCount);
        string tagSuffix = "";
        if (tags.Count > 0)
        {
            var rendered = tags.Select(t => "⚠ " + t.DisplayName());
            tagSuffix = " · " + string.Join(" · ", rendered);
        }

        return $"⊙ Scent: {Capitalize(clade)} ({Capitalize(species)}) · HP {hp}{tagSuffix}";
    }

    private static string Capitalize(string s)
    {
        if (string.IsNullOrEmpty(s)) return s;
        return char.ToUpperInvariant(s[0]) + s[1..].Replace('_', ' ');
    }

    private void Refresh()
    {
        _historyPanel.Widgets.Clear();
        _optionsPanel.Widgets.Clear();

        if (_runner is null)
        {
            _historyPanel.Widgets.Add(new Label
            {
                Text      = "(They have nothing to say yet.)",
                TextColor = new Color(180, 180, 180),
            });
            _historyPanel.Widgets.Add(new Label
            {
                Text      = "— No dialogue tree authored for this NPC yet. (Phase 6 M3 ships generic_merchant/villager/guard.)",
                TextColor = new Color(120, 110, 100),
                Wrap      = true,
            });
            var close = new TextButton
            {
                Text                = "1. Goodbye",
                Width               = 240,
                HorizontalAlignment = HorizontalAlignment.Center,
            };
            close.Click += (_, _) => _game.Screens.Pop();
            _optionsPanel.Widgets.Add(close);
            return;
        }

        // Render history (last DIALOGUE_HISTORY_LINES entries).
        int start = System.Math.Max(0, _runner.History.Count - Theriapolis.Core.C.DIALOGUE_HISTORY_LINES);
        for (int i = start; i < _runner.History.Count; i++)
        {
            var entry = _runner.History[i];
            string prefix = entry.Speaker switch
            {
                DialogueSpeaker.Pc        => "  > ",
                DialogueSpeaker.Narration => "    ",
                _                          => "",
            };
            Color color = entry.Speaker switch
            {
                DialogueSpeaker.Npc       => new Color(220, 220, 200),
                DialogueSpeaker.Pc        => new Color(170, 200, 220),
                DialogueSpeaker.Narration => new Color(160, 180, 140),
                _                          => Color.White,
            };
            _historyPanel.Widgets.Add(new Label { Text = prefix + entry.Text, Wrap = true, Width = 680, TextColor = color });
        }

        if (_runner.IsOver)
        {
            var close = new TextButton
            {
                Text                = "1. (close)",
                Width               = 240,
                HorizontalAlignment = HorizontalAlignment.Center,
            };
            close.Click += (_, _) => _game.Screens.Pop();
            _optionsPanel.Widgets.Add(close);
            return;
        }

        // Render options: number them by DISPLAY index (visible only).
        int displayN = 0;
        foreach (var (origIdx, opt) in _runner.VisibleOptions())
        {
            displayN++;
            int captured = origIdx;
            string label = $"{displayN}. {opt.Text}";
            if (opt.SkillCheck is { } sc)
                label = $"{displayN}. [{sc.Skill.ToUpperInvariant()} DC {sc.Dc}] {opt.Text}";
            var btn = new TextButton
            {
                Text                = label,
                Width               = 680,
                HorizontalAlignment = HorizontalAlignment.Center,
            };
            btn.Click += (_, _) => OnOptionPicked(captured);
            _optionsPanel.Widgets.Add(btn);
            if (displayN >= Theriapolis.Core.C.DIALOGUE_MAX_OPTIONS_PER_NODE) break;
        }
    }

    private void OnOptionPicked(int origIndex)
    {
        if (_runner is null) return;
        _runner.ChooseOption(origIndex);

        // Phase 6 M4 — dialogue's start_quest effects buffer quest ids on
        // the runner context. Drain them into the live engine before
        // refreshing, so journal entries print in the right order.
        if (_playScreen is not null && _runner.Context.StartQuestRequests.Count > 0)
        {
            var qctx = _playScreen.BuildQuestContextForDialogue();
            if (qctx is not null)
            {
                foreach (var qid in _runner.Context.StartQuestRequests)
                    _playScreen.QuestEngine.Start(qid, qctx);
            }
            _runner.Context.StartQuestRequests.Clear();
        }

        Refresh();

        // Effects may have flipped DialogueContext.ShopRequested. Push the
        // shop modal and clear the flag so re-entry doesn't loop.
        if (_runner.Context.ShopRequested
            && _content is not null
            && _playScreen?.PlayerCharacter() is { } pcChar)
        {
            _runner.Context.ShopRequested = false;
            _game.Screens.Push(new ShopScreen(_npc, pcChar, _content, _playScreen));
        }
    }

    public void Update(GameTime gt)
    {
        var ks = Keyboard.GetState();
        bool esc = ks.IsKeyDown(Keys.Escape);
        bool f   = ks.IsKeyDown(Keys.F);
        bool ent = ks.IsKeyDown(Keys.Enter);
        bool escPressed = esc && !_escWasDown;
        bool fPressed   = f   && !_fWasDown;
        bool entPressed = ent && !_enterWasDown;
        _escWasDown = esc; _fWasDown = f; _enterWasDown = ent;

        if (escPressed || fPressed) { _game.Screens.Pop(); return; }

        // Number-key option picks (edge-detected so a held key fires once).
        if (_runner is not null && !_runner.IsOver)
        {
            for (int n = 1; n <= 9; n++)
            {
                Keys k1 = (Keys)((int)Keys.D0 + n);
                Keys k2 = (Keys)((int)Keys.NumPad0 + n);
                bool down = ks.IsKeyDown(k1) || ks.IsKeyDown(k2);
                bool pressed = down && !_numWasDown[n];
                _numWasDown[n] = down;
                if (pressed)
                {
                    HandleNumberPick(n);
                    return;
                }
            }
        }
        else if (_runner is { IsOver: true } && entPressed)
        {
            _game.Screens.Pop();
        }
    }

    private void HandleNumberPick(int displayN)
    {
        if (_runner is null) return;
        int seen = 0;
        foreach (var (origIdx, _) in _runner.VisibleOptions())
        {
            seen++;
            if (seen == displayN)
            {
                OnOptionPicked(origIdx);
                return;
            }
        }
    }

    public void Draw(GameTime gt, SpriteBatch sb) => _desktop.Render();
    public void Deactivate() { }
    public void Reactivate() { Refresh(); }

    // ── Helpers ──────────────────────────────────────────────────────────

    private static string FormatRoleLine(string roleTag)
    {
        if (string.IsNullOrEmpty(roleTag)) return "";
        int dot = roleTag.LastIndexOf('.');
        if (dot < 0) return TitleCase(roleTag);
        string anchor = roleTag[..dot];
        string role   = roleTag[(dot + 1)..];
        return $"{TitleCase(role)} of {TitleCase(anchor)}";
    }

    private static string TitleCase(string raw)
    {
        if (string.IsNullOrEmpty(raw)) return "";
        Span<char> buf = stackalloc char[raw.Length];
        bool capNext = true;
        for (int i = 0; i < raw.Length; i++)
        {
            char c = raw[i];
            if (c == '_' || c == '.') { buf[i] = ' '; capNext = true; continue; }
            buf[i] = capNext ? char.ToUpperInvariant(c) : c;
            capNext = false;
        }
        return new string(buf);
    }
}
