using Theriapolis.Core.Data;
using Theriapolis.Core.Util;

namespace Theriapolis.Core.Entities;

/// <summary>
/// Phase 5 M5 NPC actor. Distinct from <see cref="PlayerActor"/>: stat-block
/// driven (no <see cref="Rules.Character.Character"/>), HP/AC/attacks read
/// directly from the source <see cref="NpcTemplateDef"/>. The
/// <see cref="Combatant"/> wrapper is built from this on encounter start.
///
/// AI behavior is dispatched by id (the template's <c>behavior</c> field)
/// — the ActorManager doesn't tick behaviors itself; combat does, and the
/// ChunkStreamer-driven spawn/despawn lifecycle keeps the live actor list
/// in sync with the player's tactical window.
/// </summary>
public sealed class NpcActor : Actor
{
    /// <summary>
    /// Phase 5 hostile/wild template. Mutually exclusive with <see cref="Resident"/>.
    /// One of the two must be non-null.
    /// </summary>
    public NpcTemplateDef? Template { get; }

    /// <summary>Phase 6 M1 friendly/neutral resident template.</summary>
    public ResidentTemplateDef? Resident { get; }

    /// <summary>
    /// Phase 6 M1 — display name (e.g. "Mara Threadwell"). For Phase-5 NPCs
    /// falls back to <see cref="NpcTemplateDef.Name"/>; for residents uses
    /// <see cref="ResidentTemplateDef.Name"/>.
    /// </summary>
    public string DisplayName => Resident?.Name ?? Template?.Name ?? "NPC";

    /// <summary>
    /// Phase 6 M1 — role tag (anchor-qualified for named NPCs:
    /// "millhaven.innkeeper", or generic: "shopkeeper"). Empty for hostile
    /// NPCs that don't carry a settlement role.
    /// </summary>
    public string RoleTag { get; init; } = "";

    /// <summary>Phase 6 M1 — dialogue tree id (matches dialogues/*.json). Empty → InteractionScreen falls back to placeholder.</summary>
    public string DialogueId { get; init; } = "";

    /// <summary>Phase 6 M1 — bias profile id used in disposition formula.</summary>
    public string BiasProfileId { get; init; } = "";

    /// <summary>
    /// Phase 6 M1 / M5 — faction affiliation id. Set automatically from
    /// <see cref="Data.NpcTemplateDef.Faction"/> or
    /// <see cref="Data.ResidentTemplateDef.Faction"/> at construction.
    /// Can also be set directly via init for testing / scripting.
    /// </summary>
    public string FactionId { get; init; } = "";

    /// <summary>Current HP. Mutated by combat; written back from <see cref="Rules.Combat.Combatant"/> when an encounter ends.</summary>
    public int CurrentHp { get; set; }

    /// <summary>HP at full. From whichever template populated this actor.</summary>
    public int MaxHp { get; }

    /// <summary>The chunk this NPC was spawned from + its index in <c>chunk.Spawns</c>. Null when spawned outside the chunk system.</summary>
    public Tactical.ChunkCoord? SourceChunk { get; init; }
    public int? SourceSpawnIndex { get; init; }

    /// <summary>
    /// Phase 6 M5 — settlement this NPC belongs to. Set by
    /// <see cref="Rules.Combat.ResidentInstantiator"/> at spawn time when
    /// the NPC was placed inside a building footprint. Drives
    /// <see cref="Rules.Reputation.RepPropagation"/> when computing the
    /// faction standing this NPC perceives.
    /// </summary>
    public int? HomeSettlementId { get; init; }

    /// <summary>Per-NPC AI scratchpad. Behaviors read/write here between turns.</summary>
    public AiState Ai { get; } = new();

    /// <summary>
    /// Phase 6.5 M6 — runtime scent-tag flags. Set by combat events
    /// (<see cref="HasRecentlyKilled"/> on melee kill, etc.) and
    /// surfaced through <see cref="ComputeScentTags"/>.
    ///
    /// Faction-derived tags are NOT stored here — they're computed from
    /// <see cref="FactionId"/> on demand. Only flags that result from
    /// runtime activity (kills, fleeing, low HP) live as state.
    /// </summary>
    public bool HasRecentlyKilled    { get; set; }
    public bool CarriesContrabandFlag { get; set; }

    /// <summary>
    /// Phase 6.5 M7 — sticky aggro flag set by
    /// <see cref="Rules.Reputation.BetrayalCascade"/> on a betrayed
    /// guard / patrol NPC. Once set, the NPC attacks on sight regardless
    /// of faction-standing recovery — they remember.
    ///
    /// Flag survives chunk despawn/respawn for *named* NPCs (their
    /// PersonalDisposition.Memory.betrayed_me tag drives re-application
    /// at re-instantiation). Generic respawning NPCs lose it on chunk
    /// re-stream — they're literally a fresh template-rolled NPC, the
    /// betrayal followed the role tag, not the entity.
    /// </summary>
    public bool PermanentAggroAfterBetrayal { get; set; }

    /// <summary>Behavior id used by combat AI dispatch. Residents return "resident" (M1 stub — they don't move).</summary>
    public string BehaviorId => Template?.Behavior ?? "resident";

    public override bool IsAlive => CurrentHp > 0;

    public NpcActor(NpcTemplateDef template)
    {
        Template   = template ?? throw new System.ArgumentNullException(nameof(template));
        Resident   = null;
        CurrentHp  = template.Hp;
        MaxHp      = template.Hp;
        Allegiance = Rules.Character.AllegianceExtensions.FromJson(template.DefaultAllegiance);
        // Phase 6 M5 — pick up the template's faction id so propagation can
        // re-classify (e.g. militia_patrol → covenant_enforcers).
        FactionId  = template.Faction;
    }

    /// <summary>Phase 6 M1 — resident-template constructor.</summary>
    public NpcActor(ResidentTemplateDef resident)
    {
        Template      = null;
        Resident      = resident ?? throw new System.ArgumentNullException(nameof(resident));
        CurrentHp     = resident.Hp;
        MaxHp         = resident.Hp;
        RoleTag       = resident.RoleTag;
        DialogueId    = resident.Dialogue;
        BiasProfileId = resident.BiasProfile;
        FactionId     = resident.Faction;
        Allegiance    = Rules.Character.AllegianceExtensions.FromJson(resident.DefaultAllegiance);
    }

    /// <summary>
    /// Phase 6.5 M6 — compute the ordered scent-tag list for a Scent-Broker
    /// reading this NPC. Higher-priority tags sort first; ties broken by
    /// enum order. Returns at most <paramref name="maxCount"/> entries.
    ///
    /// Standard tiers:
    ///   - Scent Literacy (level 1): maxCount = 1 — the headline read.
    ///   - Scent Mastery (master_nose, level 11): maxCount = 3 — fuller picture.
    ///
    /// Tag derivation:
    ///   - <see cref="FactionId"/> resolves to a single faction-affiliation
    ///     tag (priority 1–8 by enum order).
    ///   - <see cref="HasRecentlyKilled"/> → <see cref="ScentTag.RecentlyKilled"/>.
    ///   - HP &lt; 25% (or low-HP fleeing) → <see cref="ScentTag.Frightened"/>.
    ///   - HP &lt; 50% → <see cref="ScentTag.Wounded"/>.
    ///   - <see cref="CarriesContrabandFlag"/> → <see cref="ScentTag.CarriesContraband"/>.
    /// </summary>
    public List<ScentTag> ComputeScentTags(int maxCount = 1)
    {
        if (maxCount <= 0) return new List<ScentTag>();
        var list = new List<ScentTag>();

        // Faction-derived (priority 1–8). At most one — an NPC carries one
        // faction's chemistry strongly.
        var factionTag = ScentTagExtensions.FromFactionId(FactionId);
        if (factionTag != ScentTag.None) list.Add(factionTag);

        // Runtime-derived. Order matters — most informative first.
        if (HasRecentlyKilled) list.Add(ScentTag.RecentlyKilled);

        // Distress markers — cheaper of the two when triggered.
        float hpFraction = MaxHp > 0 ? (float)CurrentHp / MaxHp : 1f;
        if (hpFraction < 0.25f && CurrentHp > 0)
            list.Add(ScentTag.Frightened);
        else if (hpFraction < 0.50f && CurrentHp > 0)
            list.Add(ScentTag.Wounded);

        if (CarriesContrabandFlag) list.Add(ScentTag.CarriesContraband);

        // Truncate to the cap.
        if (list.Count > maxCount) list.RemoveRange(maxCount, list.Count - maxCount);
        return list;
    }
}

/// <summary>Mutable per-NPC AI state. Behaviors read/write between turns.</summary>
public sealed class AiState
{
    /// <summary>Last position where this NPC saw a hostile target. Used to chase after losing line-of-sight.</summary>
    public Vec2? LastSeenTargetPos { get; set; }

    /// <summary>Turns since LastSeenTargetPos was updated. After N turns, the NPC gives up and returns to home.</summary>
    public int TurnsSinceLastSeen { get; set; }

    /// <summary>"Home" position the NPC drifts back to (PoiGuard patrol anchor). Null = no home, just stand still.</summary>
    public Vec2? HomePos { get; set; }

    /// <summary>Currently engaged (in combat). Cleared on encounter end.</summary>
    public bool InCombat { get; set; }
}
