using Theriapolis.Core.Data;
using Theriapolis.Core.Entities;

namespace Theriapolis.Core.Rules.Reputation;

/// <summary>
/// Phase 6.5 M7 — when a player betrays a specific NPC (a
/// <see cref="RepEventKind.Betrayal"/> event with negative magnitude),
/// the betrayal doesn't stay personal. The cascade applies:
///
/// 1. **Personal disposition** drops by the event's magnitude (already
///    handled by <see cref="PersonalDisposition.Apply"/>; this layer
///    doesn't re-apply that delta).
/// 2. **Permanent memory flag** <c>"betrayed_me"</c> on the NPC's
///    personal record (also already handled by
///    <see cref="PersonalDisposition.Apply"/> via the
///    <see cref="PersonalDisposition.Betrayed"/> property — we
///    additionally write the explicit memory tag for dialogue gates that
///    check <c>has_memory_flag: betrayed_me</c>).
/// 3. **Faction propagation** — a tier-mapped negative delta is applied
///    to the betrayed NPC's primary faction; the existing opposition
///    matrix in <see cref="FactionStanding.Apply"/> handles the
///    faction-side cascade.
/// 4. **Permanent aggro** — for guards/patrols, set
///    <see cref="NpcActor.PermanentAggroAfterBetrayal"/>. They attack on
///    sight regardless of faction-standing recovery.
/// 5. **Ledger entry** — a faction-tagged event mirrors the personal
///    event so the reputation screen can show "Betrayed Asha · cost
///    -25 with Hybrid Underground" breadcrumbs.
///
/// Magnitude tier mapping (most-negative wins):
///   ≤ <see cref="C.BETRAYAL_MAGNITUDE_CRITICAL"/> → -50 faction
///   ≤ <see cref="C.BETRAYAL_MAGNITUDE_MAJOR"/>    → -30 faction
///   ≤ <see cref="C.BETRAYAL_MAGNITUDE_MODERATE"/> → -15 faction
///   ≤ <see cref="C.BETRAYAL_MAGNITUDE_MINOR"/>    →  -5 faction
///
/// The cascade is **deterministic** per the input event id — same event,
/// same outcome — so save/load round-trips reproduce identically.
/// </summary>
public static class BetrayalCascade
{
    /// <summary>
    /// Apply the cascade for an already-applied betrayal event. Caller is
    /// responsible for having <see cref="PlayerReputation.Submit"/>'d the
    /// underlying <see cref="RepEvent"/> first; this helper layers the
    /// cross-cutting consequences on top.
    ///
    /// <paramref name="npcs"/> is the live actor list — guards / patrols
    /// belonging to the betrayed NPC's faction get the permanent-aggro
    /// flag. Pass an empty enumerable when no live actors are available
    /// (Tools / tests).
    /// </summary>
    public static BetrayalCascadeResult Apply(
        RepEvent betrayalEvent,
        PlayerReputation rep,
        NpcActor? betrayedNpc,
        IEnumerable<NpcActor> npcs,
        IReadOnlyDictionary<string, FactionDef> factions)
    {
        if (betrayalEvent is null) throw new System.ArgumentNullException(nameof(betrayalEvent));
        if (betrayalEvent.Kind != RepEventKind.Betrayal || betrayalEvent.Magnitude >= 0)
            return BetrayalCascadeResult.Empty;

        // 1 + 2: PersonalDisposition.Apply already wrote the magnitude AND
        //        flipped Betrayed=true. The dialogue layer reads
        //        Memory.Contains("betrayed_me"); ensure the explicit tag is
        //        present (Apply only writes implicit flags).
        if (!string.IsNullOrEmpty(betrayalEvent.RoleTag))
            rep.PersonalFor(betrayalEvent.RoleTag).Memory.Add("betrayed_me");

        // 3: Faction propagation. Pick the tier from the magnitude; map to
        //    the faction-side delta; apply via FactionStanding.Apply (which
        //    cascades through the opposition matrix automatically).
        int factionDelta  = ResolveFactionDelta(betrayalEvent.Magnitude);
        string targetFaction = ResolveFactionForBetrayal(betrayedNpc, betrayalEvent);
        var factionDeltas = new List<(string FactionId, int Delta)>();
        if (!string.IsNullOrEmpty(targetFaction) && factionDelta != 0)
        {
            factionDeltas = rep.Factions.Apply(targetFaction, factionDelta, factions);

            // Mirror to the ledger as a separate, faction-tagged event so
            // the reputation screen can answer "why did Hybrid Underground
            // cool to you?" with "you betrayed Asha".
            rep.Ledger.Append(new RepEvent
            {
                Kind             = RepEventKind.Betrayal,
                FactionId        = targetFaction,
                RoleTag          = betrayalEvent.RoleTag,
                Magnitude        = factionDelta,
                Note             = $"betrayal cascade ({betrayalEvent.Note})",
                OriginTileX      = betrayalEvent.OriginTileX,
                OriginTileY      = betrayalEvent.OriginTileY,
                TimestampSeconds = betrayalEvent.TimestampSeconds,
            });
        }

        // 4: Permanent aggro for guards/patrols belonging to the same faction
        //    as the betrayed NPC. Read npc.BehaviorId to identify guard-style
        //    NPCs (brigand / patrol / poi_guard); friendly merchants /
        //    residents don't go full-aggro on betrayal.
        int aggroFlipped = 0;
        if (!string.IsNullOrEmpty(targetFaction))
        {
            foreach (var npc in npcs)
            {
                if (!npc.IsAlive) continue;
                if (npc.PermanentAggroAfterBetrayal) continue;     // already flagged
                if (string.IsNullOrEmpty(npc.FactionId)) continue;
                if (!string.Equals(npc.FactionId, targetFaction,
                        System.StringComparison.OrdinalIgnoreCase)) continue;
                if (!IsAggroEligibleBehavior(npc)) continue;
                npc.PermanentAggroAfterBetrayal = true;
                aggroFlipped++;
            }
        }

        return new BetrayalCascadeResult(
            personalRoleTag: betrayalEvent.RoleTag,
            personalMagnitude: betrayalEvent.Magnitude,
            factionId: targetFaction,
            factionDeltas: factionDeltas,
            permanentAggroFlipped: aggroFlipped);
    }

    /// <summary>
    /// Tier the personal-disposition magnitude into the faction-side delta.
    /// "Most-negative wins" — the player's worst-case betrayal sets the
    /// floor; lighter betrayals get smaller cascades.
    /// </summary>
    public static int ResolveFactionDelta(int personalMagnitude)
    {
        if (personalMagnitude >= 0) return 0;       // not a betrayal
        if (personalMagnitude <= C.BETRAYAL_MAGNITUDE_CRITICAL)
            return C.BETRAYAL_FACTION_DELTA_CRITICAL;
        if (personalMagnitude <= C.BETRAYAL_MAGNITUDE_MAJOR)
            return C.BETRAYAL_FACTION_DELTA_MAJOR;
        if (personalMagnitude <= C.BETRAYAL_MAGNITUDE_MODERATE)
            return C.BETRAYAL_FACTION_DELTA_MODERATE;
        if (personalMagnitude <= C.BETRAYAL_MAGNITUDE_MINOR)
            return C.BETRAYAL_FACTION_DELTA_MINOR;
        // -1 .. -9 (below the minor threshold) — too small to cascade.
        return 0;
    }

    /// <summary>
    /// Resolve which faction takes the cascade hit. Priority:
    /// 1. The betrayed NPC's own faction id (most natural attribution).
    /// 2. The event's <see cref="RepEvent.FactionId"/> (caller-overridden).
    /// 3. Empty (no faction cascade — personal-only event).
    /// </summary>
    private static string ResolveFactionForBetrayal(NpcActor? betrayedNpc, RepEvent ev)
    {
        if (betrayedNpc is not null && !string.IsNullOrEmpty(betrayedNpc.FactionId))
            return betrayedNpc.FactionId;
        return ev.FactionId ?? "";
    }

    /// <summary>
    /// True when an NPC's behavior id makes them a candidate for the
    /// permanent-aggro flip — armed/threatening roles only. Civilian
    /// merchants / residents stay non-aggro even on betrayal.
    /// </summary>
    private static bool IsAggroEligibleBehavior(NpcActor npc)
    {
        string b = npc.BehaviorId ?? "";
        return b.Equals("brigand",      System.StringComparison.OrdinalIgnoreCase)
            || b.Equals("patrol",       System.StringComparison.OrdinalIgnoreCase)
            || b.Equals("poi_guard",    System.StringComparison.OrdinalIgnoreCase)
            || b.Equals("wild_animal",  System.StringComparison.OrdinalIgnoreCase);
    }
}

/// <summary>Componentised result of one cascade application — used by tests + UI surfacing.</summary>
public readonly record struct BetrayalCascadeResult(
    string                            personalRoleTag,
    int                               personalMagnitude,
    string                            factionId,
    List<(string FactionId, int Delta)> factionDeltas,
    int                               permanentAggroFlipped)
{
    /// <summary>True if the cascade had no effect (e.g. magnitude ≥ 0, or no faction).</summary>
    public bool IsEmpty => factionDeltas.Count == 0 && permanentAggroFlipped == 0;

    public static BetrayalCascadeResult Empty =>
        new("", 0, "", new List<(string, int)>(), 0);
}
