# Notification Diff Batching Optimization

## Problem

`ActionResultFilter.filterForPlayer` generates per-action-result game state diffs, which is expensive. Profiling shows significant time spent in `filteredGameStateDiff` → `GameStateViewFilter.filteredGameState` (called twice per result) → `GameStateViewDiffer.diff`.

A potential optimization is to batch diffs: instead of computing N diffs for N action results, compute one combined diff representing the final state change. However, this is blocked by how client notification generators work.

## Current Architecture

### Server Side
1. `ActionResultFilter.filterForPlayer` processes each action result
2. For each result, computes `filteredGameStateDiff(before, after, factionId)`
3. Returns `Vector[ActionResultView]` where each view has its own `gameStateDiff`

### Client Side
1. `EagleGameModel.HandleNewHistoryEntry` processes each `ActionResultView`:
   ```csharp
   private void HandleNewHistoryEntry(ActionResultView arv) {
       _currentModel.HistoryCount++;
       MaybeSendNotification(arv);        // Uses currentModel state
       ApplyGameStateViewDiff(arv.GameStateDiff);  // Updates currentModel
   }
   ```

2. Notification generators receive `(ActionResultView, IGameModel)` and look up display data from the model:
   ```csharp
   var province = currentModel.Provinces[details.ProvinceId];
   var factionName = currentModel.FactionName(details.FactionId);
   var hero = currentModel.Heroes[heroId];
   var affectedProvinces = currentModel.ProvincesForFaction(factionId);
   ```

### The Problem
Notification for action N sees model state after actions 1..N-1 have been applied. If we batch diffs, notification N would see model state before ANY actions, potentially showing stale data.

Example:
1. Action 1: Province X conquered by Faction B (was Faction A)
2. Action 2: Notification needs to show Province X's current ruler

With batching, action 2's notification would incorrectly show Faction A.

## Audit of Client Notification Generators

~50 generators access `currentModel`. Key lookup patterns:

| Lookup | Count | Mutable? | Risk |
|--------|-------|----------|------|
| `currentModel.PlayerId` | 22 | No | Safe |
| `currentModel.Provinces[id]` | 17 | Yes | `RulingFactionId` changes on conquest |
| `currentModel.Heroes[id]` | ~20 | Mostly safe | Hero data stable, used for display |
| `currentModel.FactionName(id)` | ~10 | No | Names don't change |
| `currentModel.MaybeDestroyedFaction(id)` | ~15 | Yes | Faction may be destroyed |
| `currentModel.ProvincesForFaction(id)` | ~15 | Yes | Changes on conquest |

### State-Changing Action Types
- `ProvinceConquered` - changes province ownership
- `FactionDestroyed` - removes faction
- `FactionLeaderRemoved` - changes faction head

## Proposed Solution: Server-Side Display Data

Eliminate client model lookups by including all display data in server-generated notifications.

### Current Notification Structure
```scala
case class NotificationC(
    details: NotificationDetails,
    targetFactionIds: Vector[FactionId],
    affectedProvinceIds: Vector[ProvinceId],  // Already exists, underused
    affectedHeroIds: Vector[HeroId],          // Already exists, underused
    llm: NotificationT.Llm,
    deferred: Boolean
)
```

### Proposed Changes

**Option A: Add fields to NotificationC**
```scala
case class NotificationC(
    details: NotificationDetails,
    targetFactionIds: Vector[FactionId],
    affectedProvinceIds: Vector[ProvinceId],
    affectedHeroIds: Vector[HeroId],
    // New fields:
    factionNames: Map[FactionId, String],
    displayedHeroViews: Vector[HeroView],
    provinceNames: Map[ProvinceId, String],
    llm: NotificationT.Llm,
    deferred: Boolean
)
```

**Option B: Enrich each NotificationDetails type**
```scala
case class TruceAccepted(
    offeringFactionId: FactionId,
    offeringFactionName: String,      // New
    targetFactionId: FactionId,
    targetFactionName: String,        // New
    ambassadorHeroId: HeroId,
    ambassadorHeroView: HeroView      // New
)
```

### Implementation Steps

1. **Proto changes**: Add new fields to `Notification` message
2. **Server**: Populate display fields when creating notifications
3. **Client**: Update ~50 generators to use notification fields
4. **Test**: Add test that greps for `currentModel.` access in generators (excluding `PlayerId`)
5. **Server optimization**: With client decoupled from model state, batch diffs in `filterForPlayer`

### Trade-offs

**Pros:**
- Clean separation: server provides all display data
- Enables diff batching optimization
- Easier to reason about notification correctness
- Test can enforce the invariant

**Cons:**
- Larger notification messages (includes names, hero views)
- Proto changes required
- ~50 generators need updating (mechanical but tedious)
- Server must know what display data each notification type needs

## Alternative Approaches Considered

### A: Selective Per-Action Diffs
Only generate individual diffs for action results with notifications that need mutable model state. Requires tracking which notification types need which state.

**Rejected because:** Fragile; easy to add a new generator that breaks the invariant.

### B: State-Change-Triggered Diffs
If batch contains state-changing action types (ProvinceConquered, etc.), generate individual diffs from that point. Otherwise batch.

**Rejected because:** Still conservative; many batches would fall back to individual diffs.

## Status

**Deferred** - Current performance is acceptable. This doc captures the analysis for future reference if optimization becomes necessary.

## References

- `ActionResultFilter.scala` - Server-side filtering
- `EagleGameModel.cs` - Client-side model updates
- `Assets/Eagle/Notifications/` - All notification generators
- `NotificationT.scala` - Server notification types
