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

namespace Theriapolis.Core.Rules.Dialogue;

/// <summary>
/// Phase 6 M3 — walks a <see cref="DialogueDef"/> graph, evaluates option
/// conditions, branches skill checks against a deterministic dice
/// stream, and applies effects.
///
/// Determinism:
///   <c>dialogueSeed = worldSeed ^ C.RNG_DIALOGUE ^ npcId ^ turnIndex</c>
/// Each skill-check option pulls a fresh d20 keyed by
/// <c>(npcId, turnIndex, optionIndex)</c> — the cache means re-rendering
/// the same node (e.g. tooltip refresh) doesn't re-roll.
///
/// The runner does not own UI. It exposes <see cref="CurrentNode"/> and
/// <see cref="VisibleOptions"/> for the screen to render, plus <see
/// cref="History"/> for scrollback. The screen calls <see cref="ChooseOption"/>
/// when the player picks one; the runner returns a result describing
/// what happened (text to append, skill-check rolled, dialogue ended,
/// shop requested).
/// </summary>
public sealed class DialogueRunner
{
    private readonly DialogueDef     _tree;
    private readonly DialogueContext _ctx;
    private readonly ulong           _worldSeed;
    private readonly ulong           _npcId;
    private readonly Dictionary<string, DialogueNodeDef> _nodesById;

    /// <summary>Cache of (turnIndex, optionIndex) → (rolled, total) so re-renders don't re-roll.</summary>
    private readonly Dictionary<(int turn, int option), SkillCheckRoll> _rollCache = new();

    public int        TurnIndex   { get; private set; }
    public DialogueNodeDef CurrentNode { get; private set; }
    public List<DialogueLogEntry> History { get; } = new();
    public bool       IsOver      { get; private set; }

    /// <summary>Direct accessor to the runtime context — exposed so the UI
    /// can read <see cref="DialogueContext.ShopRequested"/> after option selection.</summary>
    public DialogueContext Context => _ctx;

    public DialogueRunner(DialogueDef tree, DialogueContext ctx, ulong worldSeed)
    {
        _tree      = tree ?? throw new System.ArgumentNullException(nameof(tree));
        _ctx       = ctx  ?? throw new System.ArgumentNullException(nameof(ctx));
        _worldSeed = worldSeed;
        _npcId     = (ulong)ctx.Npc.Id;

        _nodesById = tree.Nodes.ToDictionary(n => n.Id, System.StringComparer.OrdinalIgnoreCase);
        if (!_nodesById.TryGetValue(tree.Root, out var root))
            throw new System.InvalidOperationException($"Dialogue '{tree.Id}' root '{tree.Root}' missing");

        CurrentNode = root;
        AppendNodeText(root);
        ApplyEffects(root.OnEnter);
    }

    /// <summary>Options that pass their visibility predicates at the current turn.</summary>
    public IEnumerable<(int Index, DialogueOptionDef Option)> VisibleOptions()
    {
        for (int i = 0; i < CurrentNode.Options.Length; i++)
        {
            var opt = CurrentNode.Options[i];
            if (AreConditionsMet(opt.Conditions))
                yield return (i, opt);
        }
    }

    /// <summary>
    /// Pick an option by its index *into the original options array* (not
    /// the visible-only list — index stability across re-renders).
    /// </summary>
    public DialogueChooseResult ChooseOption(int optionIndex)
    {
        if (IsOver) return DialogueChooseResult.Closed("(dialogue is already over)");
        if (optionIndex < 0 || optionIndex >= CurrentNode.Options.Length)
            return DialogueChooseResult.Closed("(no such option)");
        var opt = CurrentNode.Options[optionIndex];
        if (!AreConditionsMet(opt.Conditions))
            return DialogueChooseResult.Closed("(option not available)");

        // Append the player's choice to history.
        History.Add(new DialogueLogEntry(DialogueSpeaker.Pc, opt.Text));
        TurnIndex++;

        // Skill-check option: roll, branch on success/failure, apply
        // appropriate effects/next.
        if (opt.SkillCheck is { } check)
        {
            var roll = ResolveSkillCheck(optionIndex, check);
            string log = $"  [{check.Skill.ToUpperInvariant()} DC {check.Dc}] roll {roll.D20Raw} + {roll.Bonus} = {roll.Total} → {(roll.Succeeded ? "SUCCESS" : "FAILURE")}";
            History.Add(new DialogueLogEntry(DialogueSpeaker.Narration, log));
            ApplyEffects(roll.Succeeded ? opt.EffectsOnSuccess : opt.EffectsOnFailure);
            string nextId = roll.Succeeded ? opt.NextOnSuccess : opt.NextOnFailure;
            return AdvanceTo(nextId, roll);
        }

        // Plain option.
        ApplyEffects(opt.Effects);
        return AdvanceTo(opt.Next, default);
    }

    /// <summary>Force-close the dialogue (player pressed Esc).</summary>
    public void End()
    {
        if (IsOver) return;
        IsOver = true;
    }

    // ── Conditions ───────────────────────────────────────────────────────

    private bool AreConditionsMet(DialogueConditionDef[] conditions)
    {
        foreach (var c in conditions)
            if (!Evaluate(c)) return false;
        return true;
    }

    private bool Evaluate(DialogueConditionDef c) => c.Kind.ToLowerInvariant() switch
    {
        "rep_at_least" => RepFor(c.Faction) >= c.Value,
        "rep_below"    => RepFor(c.Faction) <  c.Value,
        "has_item"     => HasItem(c.Id),
        "not_has_item" => !HasItem(c.Id),
        "has_flag"     => _ctx.Flags.TryGetValue(c.Flag, out int v) && v != 0,
        "not_has_flag" => !_ctx.Flags.TryGetValue(c.Flag, out int v2) || v2 == 0,
        "ability_min"  => AbilityMod(c.Ability) >= c.Value,
        _              => true,    // unknown kind → permissive (validated at content-load)
    };

    private int RepFor(string faction)
    {
        if (string.IsNullOrEmpty(faction))
            return _ctx.EffectiveDispositionScore();
        return _ctx.Reputation.Factions.Get(faction);
    }

    private bool HasItem(string itemId)
    {
        if (string.IsNullOrEmpty(itemId)) return false;
        foreach (var inst in _ctx.Pc.Inventory.Items)
            if (string.Equals(inst.Def.Id, itemId, System.StringComparison.OrdinalIgnoreCase))
                return true;
        return false;
    }

    private int AbilityMod(string abilityRaw)
    {
        if (!System.Enum.TryParse<AbilityId>(abilityRaw, true, out var id)) return 0;
        return _ctx.Pc.Abilities.ModFor(id);
    }

    // ── Effects ──────────────────────────────────────────────────────────

    private void ApplyEffects(DialogueEffectDef[] effects)
    {
        foreach (var e in effects) ApplyEffect(e);
    }

    private void ApplyEffect(DialogueEffectDef e)
    {
        switch (e.Kind.ToLowerInvariant())
        {
            case "set_flag":
                _ctx.Flags[e.Flag] = e.Value;
                break;
            case "clear_flag":
                _ctx.Flags.Remove(e.Flag);
                break;
            case "give_item":
                if (_ctx.Content.Items.TryGetValue(e.Id, out var giveDef))
                    _ctx.Pc.Inventory.Add(giveDef, System.Math.Max(1, e.Qty));
                break;
            case "take_item":
                TakeFromInventory(e.Id, System.Math.Max(1, e.Qty));
                break;
            case "rep_event":
                if (e.Event is { } ev)
                    SubmitRepEvent(ev);
                break;
            case "open_shop":
                _ctx.ShopRequested = true;
                break;
            case "start_quest":
                if (!string.IsNullOrEmpty(e.Quest))
                    _ctx.StartQuestRequests.Add(e.Quest);
                break;
            case "give_xp":
                _ctx.Pc.Xp = System.Math.Max(0, _ctx.Pc.Xp + e.Xp);
                break;
        }
    }

    private void TakeFromInventory(string itemId, int qty)
    {
        if (string.IsNullOrEmpty(itemId)) return;
        for (int i = _ctx.Pc.Inventory.Items.Count - 1; i >= 0 && qty > 0; i--)
        {
            var inst = _ctx.Pc.Inventory.Items[i];
            if (!string.Equals(inst.Def.Id, itemId, System.StringComparison.OrdinalIgnoreCase)) continue;
            int take = System.Math.Min(qty, inst.Qty);
            inst.Qty -= take;
            qty      -= take;
            if (inst.Qty <= 0) _ctx.Pc.Inventory.Remove(inst);
        }
    }

    private void SubmitRepEvent(DialogueRepEventDef ev)
    {
        if (!System.Enum.TryParse<RepEventKind>(ev.Kind, true, out var kind)) kind = RepEventKind.Dialogue;
        var live = new RepEvent
        {
            Kind             = kind,
            FactionId        = ev.Faction,
            RoleTag          = string.IsNullOrEmpty(ev.RoleTag) ? _ctx.Npc.RoleTag : ev.RoleTag,
            Magnitude        = ev.Magnitude,
            Note             = ev.Note,
            OriginTileX      = _ctx.PlayerWorldTileX,
            OriginTileY      = _ctx.PlayerWorldTileY,
            TimestampSeconds = _ctx.WorldClockSeconds,
        };
        _ctx.Reputation.Submit(live, _ctx.Content.Factions);
    }

    // ── Skill check ──────────────────────────────────────────────────────

    private SkillCheckRoll ResolveSkillCheck(int optionIndex, DialogueSkillCheckDef check)
    {
        var key = (TurnIndex - 1, optionIndex);
        if (_rollCache.TryGetValue(key, out var cached)) return cached;

        var skill = SkillIdExtensions.FromJson(check.Skill);
        int abilityMod = _ctx.Pc.Abilities.ModFor(skill.Ability());
        int profBonus  = _ctx.Pc.SkillProficiencies.Contains(skill) ? _ctx.Pc.ProficiencyBonus : 0;
        int bonus      = abilityMod + profBonus;

        ulong seed = _worldSeed
            ^ C.RNG_DIALOGUE
            ^ _npcId
            ^ ((ulong)(uint)key.Item1 << 8)
            ^ ((ulong)(uint)key.Item2 << 24);
        var rng = new SeededRng(seed);
        int d20 = (int)(rng.NextUInt64() % 20UL) + 1;
        int total = d20 + bonus;

        var roll = new SkillCheckRoll(skill, check.Dc, d20, bonus, total, total >= check.Dc);
        _rollCache[key] = roll;
        return roll;
    }

    // ── Node transitions ─────────────────────────────────────────────────

    private DialogueChooseResult AdvanceTo(string nextId, SkillCheckRoll skillRoll)
    {
        if (string.IsNullOrEmpty(nextId) || string.Equals(nextId, "<end>", System.StringComparison.OrdinalIgnoreCase))
        {
            IsOver = true;
            return DialogueChooseResult.Closed(skillRoll.Skill == 0 ? "" : "");
        }
        if (!_nodesById.TryGetValue(nextId, out var next))
        {
            IsOver = true;
            return DialogueChooseResult.Closed($"(missing node '{nextId}' — content bug)");
        }
        CurrentNode = next;
        AppendNodeText(next);
        ApplyEffects(next.OnEnter);
        return DialogueChooseResult.Advanced(skillRoll);
    }

    private void AppendNodeText(DialogueNodeDef node)
    {
        var speaker = node.Speaker.ToLowerInvariant() switch
        {
            "pc"        => DialogueSpeaker.Pc,
            "narration" => DialogueSpeaker.Narration,
            _            => DialogueSpeaker.Npc,
        };
        History.Add(new DialogueLogEntry(speaker, ResolvePlaceholders(node.Text)));
    }

    /// <summary>
    /// Substitute placeholders in dialogue text. Phase 6 M3 supports
    /// {pc.name}, {npc.role}, {npc.name}, {disposition_label}.
    /// </summary>
    private string ResolvePlaceholders(string text)
    {
        if (string.IsNullOrEmpty(text)) return text;
        return text
            .Replace("{pc.name}",          _ctx.Pc is null ? "Wanderer" : "the wanderer", System.StringComparison.OrdinalIgnoreCase)
            .Replace("{npc.role}",         _ctx.Npc.RoleTag ?? "", System.StringComparison.OrdinalIgnoreCase)
            .Replace("{npc.name}",         _ctx.Npc.DisplayName, System.StringComparison.OrdinalIgnoreCase)
            .Replace("{disposition_label}",
                DispositionLabels.DisplayName(DispositionLabels.For(_ctx.EffectiveDispositionScore())),
                System.StringComparison.OrdinalIgnoreCase);
    }
}

public enum DialogueSpeaker : byte { Npc, Pc, Narration }

public readonly record struct DialogueLogEntry(DialogueSpeaker Speaker, string Text);

public readonly record struct SkillCheckRoll(SkillId Skill, int Dc, int D20Raw, int Bonus, int Total, bool Succeeded);

public readonly struct DialogueChooseResult
{
    public bool             ClosedAfter { get; }
    public string           Note        { get; }
    public SkillCheckRoll?  Roll        { get; }

    private DialogueChooseResult(bool closed, string note, SkillCheckRoll? roll)
    {
        ClosedAfter = closed;
        Note        = note;
        Roll        = roll;
    }

    public static DialogueChooseResult Closed(string note)
        => new(true, note, null);

    public static DialogueChooseResult Advanced(SkillCheckRoll roll)
        => new(false, "", roll.Dc == 0 ? null : roll);
}
