using Theriapolis.Core.Data;
using Theriapolis.Core.Rules.Reputation;

namespace Theriapolis.Core.Rules.Quests;

/// <summary>
/// Phase 6 M4 — quest engine. Owns the active + completed quest lists.
/// On each <see cref="Tick"/> it walks active quests, evaluates each
/// step's <see cref="QuestStepDef.TriggerConditions"/>, fires
/// <see cref="QuestStepDef.OnEnter"/>+outcomes when ready, and chains
/// transitions until no further step fires this tick.
///
/// The engine is intentionally not a script interpreter — every trigger
/// and effect is one of a closed set of enum-tagged kinds (plan §8: hard
/// rule). New behaviour goes in via a new kind, never via dynamic code.
/// </summary>
public sealed class QuestEngine
{
    private readonly Dictionary<string, QuestState> _active    = new(System.StringComparer.OrdinalIgnoreCase);
    private readonly Dictionary<string, QuestState> _completed = new(System.StringComparer.OrdinalIgnoreCase);

    public IReadOnlyDictionary<string, QuestState> Active    => _active;
    public IReadOnlyDictionary<string, QuestState> Completed => _completed;

    /// <summary>Append-only player-facing log of "milestones reached" for the journal screen.</summary>
    public List<string> Journal { get; } = new();

    public QuestStatus StatusOf(string questId)
    {
        if (_active.TryGetValue(questId, out var a))    return a.Status;
        if (_completed.TryGetValue(questId, out var c)) return c.Status;
        return QuestStatus.Active;   // sentinel for "never started" — caller checks IsKnown
    }

    public bool IsActive(string questId)    => _active.ContainsKey(questId);
    public bool IsCompleted(string questId) => _completed.TryGetValue(questId, out var c) && c.Status == QuestStatus.Completed;
    public bool IsFailed(string questId)    => _completed.TryGetValue(questId, out var c) && c.Status == QuestStatus.Failed;

    public QuestState? Get(string questId)
        => _active.TryGetValue(questId, out var a) ? a
         : _completed.TryGetValue(questId, out var c) ? c
         : null;

    /// <summary>
    /// Start a quest. Idempotent — re-starting an already-active quest is
    /// a no-op; re-starting a completed quest is also a no-op (Phase 6
    /// M4 has no "redo" semantics).
    /// </summary>
    public bool Start(string questId, QuestContext ctx)
    {
        if (_active.ContainsKey(questId) || _completed.ContainsKey(questId)) return false;
        if (!ctx.Content.Quests.TryGetValue(questId, out var def)) return false;
        if (_active.Count >= C.QUEST_MAX_ACTIVE) return false;

        var state = new QuestState
        {
            QuestId      = questId,
            CurrentStep  = def.EntryStep,
            Status       = QuestStatus.Active,
            StartedAt    = ctx.Clock.InGameSeconds,
            StepStartedAt = ctx.Clock.InGameSeconds,
        };
        _active[questId] = state;
        Journal.Add($"Started: {def.Title}");

        // Run on_enter for the entry step immediately so the quest can do
        // setup (set_flag, give_item, etc.) before its first tick.
        if (FindStep(def, def.EntryStep) is { } entry)
            ApplyEffects(entry.OnEnter, ctx, state);

        // The entry step might itself satisfy its outcomes immediately
        // (e.g. trigger conditions all already met). Run a follow-up tick.
        TickQuest(def, state, ctx);
        return true;
    }

    /// <summary>End an active quest manually (e.g. dialogue effect or quest-failure step).</summary>
    public void End(string questId, QuestStatus status, QuestContext ctx)
    {
        if (!_active.TryGetValue(questId, out var state)) return;
        if (!ctx.Content.Quests.TryGetValue(questId, out var def)) return;
        FinishQuest(def, state, status);
    }

    /// <summary>Per-frame tick. Runs auto-start checks then advances every active quest.</summary>
    public void Tick(QuestContext ctx)
    {
        // Auto-start checks: any quest whose AutoStartWhen conditions all
        // pass and which isn't yet active or completed kicks off.
        foreach (var def in ctx.Content.Quests.Values)
        {
            if (def.AutoStartWhen.Length == 0) continue;
            if (_active.ContainsKey(def.Id) || _completed.ContainsKey(def.Id)) continue;
            if (AreConditionsMet(def.AutoStartWhen, ctx))
                Start(def.Id, ctx);
        }

        // Advance each active quest. Collect first to allow Tick → end-quest
        // → modify _active mid-iteration.
        var snap = new List<QuestState>(_active.Values);
        foreach (var state in snap)
        {
            if (!ctx.Content.Quests.TryGetValue(state.QuestId, out var def)) continue;
            if (state.Status != QuestStatus.Active) continue;
            TickQuest(def, state, ctx);
        }
    }

    /// <summary>Walk one quest forward until no step fires this tick.</summary>
    private void TickQuest(QuestDef def, QuestState state, QuestContext ctx)
    {
        // Iterate so an outcome that lands on a step whose triggers also
        // fire chains into the next step in the same frame.
        int hops = 0;
        const int MaxHops = 32;   // sanity guard against pathological cycles
        while (hops++ < MaxHops)
        {
            var step = FindStep(def, state.CurrentStep);
            if (step is null) break;

            // Quest-terminal step?
            if (step.CompletesQuest) { FinishQuest(def, state, QuestStatus.Completed); return; }
            if (step.FailsQuest)     { FinishQuest(def, state, QuestStatus.Failed);    return; }

            // Triggers gate the step: if not all met, wait for next tick.
            if (step.TriggerConditions.Length > 0 && !AreConditionsMet(step.TriggerConditions, ctx))
                break;

            // Pick first outcome whose `when` clauses are all satisfied.
            QuestOutcomeDef? chosen = null;
            foreach (var o in step.Outcomes)
            {
                if (o.When.Length == 0 || AreConditionsMet(o.When, ctx))
                {
                    chosen = o;
                    break;
                }
            }
            if (chosen is null) break;

            ApplyEffects(chosen.Effects, ctx, state);
            string? nextId = string.Equals(chosen.Next, "<end>", System.StringComparison.OrdinalIgnoreCase)
                ? null
                : chosen.Next;
            if (nextId is null) { FinishQuest(def, state, QuestStatus.Completed); return; }
            state.CurrentStep   = nextId;
            state.StepStartedAt = ctx.Clock.InGameSeconds;
            Journal.Add($"  → {def.Id}: '{nextId}'");
            // Run on_enter for the new step.
            if (FindStep(def, nextId) is { } newStep)
                ApplyEffects(newStep.OnEnter, ctx, state);
        }
    }

    private void FinishQuest(QuestDef def, QuestState state, QuestStatus status)
    {
        state.Status = status;
        _active.Remove(def.Id);
        // Cap completed history to keep save size bounded.
        if (_completed.Count >= C.QUEST_LOG_COMPLETED_LIMIT)
        {
            // Drop the oldest (insertion order via OrderBy on StartedAt).
            string? oldest = _completed.Values.OrderBy(s => s.StartedAt).FirstOrDefault()?.QuestId;
            if (oldest is not null) _completed.Remove(oldest);
        }
        _completed[def.Id] = state;
        Journal.Add(status switch
        {
            QuestStatus.Completed => $"Completed: {def.Title}",
            QuestStatus.Failed    => $"Failed: {def.Title}",
            _                      => $"Ended: {def.Title}",
        });
    }

    private static QuestStepDef? FindStep(QuestDef def, string stepId)
    {
        foreach (var s in def.Steps)
            if (string.Equals(s.Id, stepId, System.StringComparison.OrdinalIgnoreCase))
                return s;
        return null;
    }

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

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

    private bool Evaluate(QuestConditionDef c, QuestContext ctx) => c.Kind.ToLowerInvariant() switch
    {
        "flag_set"             => ctx.Flags.TryGetValue(c.Flag, out int v)   && v != 0,
        "flag_clear"           => !ctx.Flags.TryGetValue(c.Flag, out int v2) || v2 == 0,
        "flag_at_least"        => ctx.Flags.TryGetValue(c.Flag, out int vv) && vv >= c.Value,
        "enter_anchor"         => CheckEnterAnchor(c, ctx),
        "enter_role_proximity" => CheckEnterRoleProximity(c, ctx),
        "npc_dead"             => CheckNpcAlive(c, ctx) is { } liveDead && !liveDead,
        "npc_alive"            => CheckNpcAlive(c, ctx) is { } live && live,
        "time_elapsed_seconds" => ctx.Clock.InGameSeconds >= c.Seconds,
        "rep_at_least"         => RepFor(c.Faction, ctx) >= c.Value,
        "rep_below"            => RepFor(c.Faction, ctx) <  c.Value,
        "has_item"             => ctx.HasItem(c.Id),
        "not_has_item"         => !ctx.HasItem(c.Id),
        "quest_complete"       => IsCompleted(c.Quest),
        "quest_active"         => IsActive(c.Quest),
        "dialogue_choice"      => string.Equals(ctx.LastDialogueNodeReached, c.Id,
                                                 System.StringComparison.OrdinalIgnoreCase),
        _                       => false,
    };

    private bool CheckEnterAnchor(QuestConditionDef c, QuestContext ctx)
    {
        if (string.IsNullOrEmpty(c.Anchor)) return false;
        int? sId = ctx.Anchors.ResolveAnchor(c.Anchor.StartsWith("anchor:")
            ? c.Anchor : $"anchor:{c.Anchor}");
        if (sId is null) return false;
        var dist = ctx.PlayerDistanceToSettlement(sId.Value);
        return dist is not null && dist.Value <= C.QUEST_ENTER_ANCHOR_RADIUS_TILES;
    }

    private bool CheckEnterRoleProximity(QuestConditionDef c, QuestContext ctx)
    {
        if (string.IsNullOrEmpty(c.Role)) return false;
        int? npcId = ctx.Anchors.ResolveRole(c.Role.StartsWith("role:")
            ? c.Role : $"role:{c.Role}");
        if (npcId is null) return false;
        if (ctx.PlayerTacticalPos() is not { } ppos) return false;

        foreach (var npc in ctx.Actors.Npcs)
        {
            if (npc.Id != npcId.Value) continue;
            int dx = (int)System.Math.Abs(npc.Position.X - ppos.x);
            int dy = (int)System.Math.Abs(npc.Position.Y - ppos.y);
            return System.Math.Max(dx, dy) <= C.QUEST_ENTER_ROLE_RADIUS_TILES;
        }
        return false;
    }

    private bool? CheckNpcAlive(QuestConditionDef c, QuestContext ctx)
    {
        string roleTag = string.IsNullOrEmpty(c.Role) ? c.Npc : c.Role;
        if (string.IsNullOrEmpty(roleTag)) return null;
        int? npcId = ctx.Anchors.ResolveRole(roleTag.StartsWith("role:") ? roleTag : $"role:{roleTag}");
        if (npcId is null)
        {
            // The NPC is not currently spawned; treat as ALIVE if the engine
            // hasn't recorded a kill flag (chunks evict NPCs frequently).
            return true;
        }
        foreach (var npc in ctx.Actors.Npcs)
            if (npc.Id == npcId.Value) return npc.IsAlive;
        return true;
    }

    private static int RepFor(string faction, QuestContext ctx)
    {
        if (string.IsNullOrEmpty(faction)) return 0;
        return ctx.Reputation.Factions.Get(faction);
    }

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

    private void ApplyEffects(QuestEffectDef[] effects, QuestContext ctx, QuestState state)
    {
        foreach (var e in effects) ApplyEffect(e, ctx, state);
    }

    private void ApplyEffect(QuestEffectDef e, QuestContext ctx, QuestState state)
    {
        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.PlayerCharacter is not null && ctx.Content.Items.TryGetValue(e.Id, out var giveDef))
                    ctx.PlayerCharacter.Inventory.Add(giveDef, System.Math.Max(1, e.Qty));
                break;
            case "take_item":  TakeItem(ctx, e.Id, System.Math.Max(1, e.Qty)); break;
            case "give_xp":
                if (ctx.PlayerCharacter is not null)
                    ctx.PlayerCharacter.Xp = System.Math.Max(0, ctx.PlayerCharacter.Xp + e.Xp);
                break;
            case "rep_event":  if (e.Event is { } ev) SubmitRepEvent(ev, ctx); break;
            case "start_quest": if (!string.IsNullOrEmpty(e.Quest)) Start(e.Quest, ctx); break;
            case "end_quest":   if (_active.ContainsKey(state.QuestId)) FinishQuest(
                                    ctx.Content.Quests[state.QuestId], state, QuestStatus.Completed); break;
            case "fail_quest":  if (_active.ContainsKey(state.QuestId)) FinishQuest(
                                    ctx.Content.Quests[state.QuestId], state, QuestStatus.Failed); break;
            // spawn_npc / despawn_npc are M4 stubs — Phase 6 M5 wires the
            // residency manipulation. Recording in the journal so the
            // player can see the quest *intends* it.
            case "spawn_npc":   Journal.Add($"(quest) spawn_npc {e.Role} ← {e.Template}");   break;
            case "despawn_npc": Journal.Add($"(quest) despawn_npc {e.Role}"); break;
        }
    }

    private static void TakeItem(QuestContext ctx, string itemId, int qty)
    {
        if (ctx.PlayerCharacter is null || string.IsNullOrEmpty(itemId)) return;
        var inv = ctx.PlayerCharacter.Inventory;
        for (int i = inv.Items.Count - 1; i >= 0 && qty > 0; i--)
        {
            var inst = inv.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) inv.Remove(inst);
        }
    }

    private static void SubmitRepEvent(DialogueRepEventDef ev, QuestContext ctx)
    {
        if (!System.Enum.TryParse<RepEventKind>(ev.Kind, true, out var kind)) kind = RepEventKind.Quest;
        var live = new RepEvent
        {
            Kind             = kind,
            FactionId        = ev.Faction,
            RoleTag          = ev.RoleTag,
            Magnitude        = ev.Magnitude,
            Note             = ev.Note,
            TimestampSeconds = ctx.Clock.InGameSeconds,
        };
        ctx.Reputation.Submit(live, ctx.Content.Factions);
    }

    public void Clear()
    {
        _active.Clear();
        _completed.Clear();
        Journal.Clear();
    }

    /// <summary>
    /// Save-load helpers — used by <see cref="Persistence.QuestCodec"/> to
    /// restore engine state without re-firing on_enter effects (those
    /// already applied in the saved game).
    /// </summary>
    public void AdoptActive(QuestState state)    => _active[state.QuestId]    = state;
    public void AdoptCompleted(QuestState state) => _completed[state.QuestId] = state;
}
