using Theriapolis.Core.Data;

namespace Theriapolis.Core.Rules.Reputation;

/// <summary>
/// Phase 6 M2 — Layer 2 of the disposition stack. The player's
/// faction-affiliated reputation: <c>Dictionary&lt;FactionId, int&gt;</c>
/// clamped to <c>±C.REP_MAX</c>, with the opposition matrix from
/// <c>reputation.md §I-2</c> applied automatically on every change.
///
/// Phase 6 M2 ships the score-only contract — propagation by distance
/// + time decay arrives in M5. Until then, every <see cref="Apply"/> call
/// fires an event that updates the at-origin standing immediately and
/// cascades through opposition; nothing else happens elsewhere on the map.
/// </summary>
public sealed class FactionStanding
{
    private readonly Dictionary<string, int> _standings = new(System.StringComparer.OrdinalIgnoreCase);

    /// <summary>Snapshot of every faction's current standing.</summary>
    public IReadOnlyDictionary<string, int> Standings => _standings;

    /// <summary>
    /// Score with <paramref name="factionId"/>. Returns 0 (neutral) when
    /// the faction has never been touched.
    /// </summary>
    public int Get(string factionId)
        => _standings.TryGetValue(factionId, out int v) ? v : 0;

    /// <summary>Direct setter, no opposition cascade. Used by save-load and tests.</summary>
    public void Set(string factionId, int value)
    {
        _standings[factionId] = System.Math.Clamp(value, C.REP_MIN, C.REP_MAX);
    }

    /// <summary>
    /// Apply <paramref name="delta"/> to <paramref name="factionId"/>'s
    /// standing and cascade the opposition matrix. Returns the list of
    /// (factionId, delta) tuples actually applied (caller can log them).
    ///
    /// Cascading is single-hop: <paramref name="factions"/>['inheritors'].opposition
    /// is read once. Phase 6 M2 doesn't iterate (no transitive opposition).
    /// </summary>
    public List<(string FactionId, int Delta)> Apply(
        string factionId,
        int delta,
        IReadOnlyDictionary<string, FactionDef> factions)
    {
        var applied = new List<(string, int)>();
        if (delta == 0) return applied;

        // Direct change first — clamped delta accounts for floor/ceiling.
        int actualDelta = ApplyClamped(factionId, delta);
        applied.Add((factionId, actualDelta));

        // Cascade through opposition (use the *requested* delta, not the
        // possibly-truncated one, so a clamp at the source doesn't mute
        // downstream effects too).
        if (factions.TryGetValue(factionId, out var def))
        {
            foreach (var (otherId, mult) in def.Opposition)
            {
                if (mult == 0f) continue;
                int subDelta = (int)System.Math.Round(delta * mult);
                if (subDelta == 0) continue;
                int actualSub = ApplyClamped(otherId, subDelta);
                if (actualSub != 0) applied.Add((otherId, actualSub));
            }
        }
        return applied;
    }

    private int ApplyClamped(string factionId, int delta)
    {
        int current = Get(factionId);
        int next    = System.Math.Clamp(current + delta, C.REP_MIN, C.REP_MAX);
        if (next == current) return 0;
        _standings[factionId] = next;
        return next - current;
    }

    public void Clear() => _standings.Clear();
}
