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

namespace Theriapolis.Core.Rules.Reputation;

/// <summary>
/// Phase 6 M5 — distance-banded reputation propagation per
/// <c>reputation.md §I-2</c>.
///
/// The model: every <see cref="RepEvent"/> in the
/// <see cref="RepLedger"/> is *visible everywhere* (full magnitude at
/// origin, decayed by Chebyshev tile distance to other settlements,
/// frontier settlements may not receive at all). This module computes
/// per-settlement faction standing on demand by walking the ledger and
/// summing the decayed contributions plus opposition-matrix cascades.
///
/// Determinism: frontier coin-flips are keyed by
/// <c>(worldSeed, eventSequenceId, settlementId)</c> so the same news
/// arrives (or doesn't) the same way across save/load.
///
/// Complexity: O(events × settlements × factions) for a full sweep, but
/// per-NPC-disposition queries hit only the player's home settlement
/// and run in O(events × factions) — bounded ledger size keeps it cheap.
/// </summary>
public static class RepPropagation
{
    /// <summary>
    /// Faction standing as perceived in <paramref name="settlement"/>.
    /// Walks the ledger, applies distance decay + cascade. Clamped to
    /// <c>±C.REP_MAX</c>.
    /// </summary>
    public static int LocalStandingFor(
        string factionId,
        Settlement settlement,
        ulong worldSeed,
        RepLedger ledger,
        IReadOnlyDictionary<string, FactionDef> factions)
    {
        if (string.IsNullOrEmpty(factionId)) return 0;
        if (settlement is null) return 0;
        if (ledger.Count == 0) return 0;

        int total = 0;
        foreach (var ev in ledger.Entries)
        {
            int delta = ContributionForFaction(factionId, ev, settlement, worldSeed, factions);
            total += delta;
        }
        return System.Math.Clamp(total, C.REP_MIN, C.REP_MAX);
    }

    /// <summary>
    /// Per-event contribution to a settlement's local standing for one
    /// faction. Includes both direct events (event.FactionId == faction)
    /// and cascade events (other factions whose opposition matrix names
    /// this faction). Returns 0 when the event hasn't propagated to this
    /// settlement (frontier coin-flip failure).
    /// </summary>
    public static int ContributionForFaction(
        string factionId,
        RepEvent ev,
        Settlement settlement,
        ulong worldSeed,
        IReadOnlyDictionary<string, FactionDef> factions)
    {
        if (string.IsNullOrEmpty(ev.FactionId)) return 0;

        int distTiles = ChebyshevDistance(ev.OriginTileX, ev.OriginTileY,
                                          settlement.TileX, settlement.TileY);
        bool isExtreme = System.Math.Abs(ev.Magnitude) >= C.REP_EXTREME_BYPASS_MAGNITUDE;

        // Frontier band requires a per-(event, settlement) coin flip.
        var band = BandFor(distTiles);
        if (!isExtreme && band == DistanceBand.Frontier
            && !FrontierDelivered(worldSeed, ev.SequenceId, settlement.Id))
            return 0;

        int decayPct = isExtreme ? C.REP_DECAY_AT_ORIGIN_PCT : DecayPctFor(band);

        int direct = 0;
        if (string.Equals(ev.FactionId, factionId, System.StringComparison.OrdinalIgnoreCase))
            direct = (int)System.Math.Round(ev.Magnitude * (decayPct / 100f));

        int cascade = 0;
        if (factions.TryGetValue(ev.FactionId, out var sourceDef)
            && sourceDef.Opposition.TryGetValue(factionId, out float mult)
            && mult != 0f)
        {
            cascade = (int)System.Math.Round(ev.Magnitude * mult * (decayPct / 100f));
        }

        return direct + cascade;
    }

    /// <summary>
    /// Convenience: human-readable breakdown of *why* the local standing
    /// looks the way it does. Used by the disposition tooltip and the
    /// reputation screen's "recent events" tail.
    /// </summary>
    public static IEnumerable<(RepEvent Event, int LocalDelta, DistanceBand Band)>
        ExplainLocalStanding(
            string factionId,
            Settlement settlement,
            ulong worldSeed,
            RepLedger ledger,
            IReadOnlyDictionary<string, FactionDef> factions,
            int max = 8)
    {
        if (string.IsNullOrEmpty(factionId) || settlement is null) yield break;
        int yielded = 0;
        // Most recent first.
        for (int i = ledger.Entries.Count - 1; i >= 0 && yielded < max; i--)
        {
            var ev = ledger.Entries[i];
            int delta = ContributionForFaction(factionId, ev, settlement, worldSeed, factions);
            if (delta == 0) continue;
            int distTiles = ChebyshevDistance(ev.OriginTileX, ev.OriginTileY,
                                              settlement.TileX, settlement.TileY);
            yield return (ev, delta, BandFor(distTiles));
            yielded++;
        }
    }

    public enum DistanceBand : byte
    {
        Origin      = 0,
        Adjacent    = 1,
        Regional    = 2,
        Continental = 3,
        Frontier    = 4,
    }

    public static DistanceBand BandFor(int chebyshevTiles)
    {
        if (chebyshevTiles == 0)                              return DistanceBand.Origin;
        if (chebyshevTiles <= C.REP_ADJACENT_DIST_TILES)      return DistanceBand.Adjacent;
        if (chebyshevTiles <= C.REP_REGIONAL_DIST_TILES)      return DistanceBand.Regional;
        if (chebyshevTiles <= C.REP_CONTINENTAL_DIST_TILES)   return DistanceBand.Continental;
        return DistanceBand.Frontier;
    }

    public static int DecayPctFor(DistanceBand band) => band switch
    {
        DistanceBand.Origin      => C.REP_DECAY_AT_ORIGIN_PCT,
        DistanceBand.Adjacent    => C.REP_DECAY_ADJACENT_PCT,
        DistanceBand.Regional    => C.REP_DECAY_REGIONAL_PCT,
        DistanceBand.Continental => C.REP_DECAY_CONTINENTAL_PCT,
        DistanceBand.Frontier    => C.REP_DECAY_FRONTIER_PCT,
        _                          => 0,
    };

    /// <summary>
    /// Deterministic coin-flip per <c>(worldSeed, eventSequenceId, settlementId)</c>.
    /// Returns true if the news of this event reaches the frontier
    /// settlement at all.
    /// </summary>
    public static bool FrontierDelivered(ulong worldSeed, int eventSequenceId, int settlementId)
    {
        // Mix the keys so seeds collide as rarely as possible.
        ulong mix = unchecked(worldSeed
            ^ C.RNG_REP_PROPAGATION
            ^ ((ulong)(uint)eventSequenceId << 16)
            ^ ((ulong)(uint)settlementId << 40));
        var rng = new SeededRng(mix);
        int roll = (int)(rng.NextUInt64() % 100UL);
        return roll < C.REP_FRONTIER_DELIVERY_PROB_PCT;
    }

    private static int ChebyshevDistance(int x1, int y1, int x2, int y2)
        => System.Math.Max(System.Math.Abs(x1 - x2), System.Math.Abs(y1 - y2));
}
