"""exec_reconcile.py (D2.2/2.3, Task 3) — `exec_vs_sim`: THE one shared "how closely real trades follow the
plan" signal. Reused by the holdings detail page (this task) and, per the plan, the D3/D4 overview
surfacing later — so it lives in `application/`, not inline in a route.

Compares a strategy's EXECUTED forward track's cumulative return (the `<sid>_exec` track's persisted
`forward_summary.total_return`) against its own MODELED/sim forward cumulative return (the `sid` track's
`forward_summary.total_return`) — both already computed and persisted by the forward engine elsewhere; this
module does NO new compute, only comparison, plus a read of the strategy-tagged `exec_fills` for the honest
cost-to-trade inputs (median slippage, total fees).

`has_exec` is the gate: when the `<sid>_exec` track has no persisted forward_summary row yet (a real account
that hasn't run, or an exec twin that's suspended pending a fund decision), there is nothing to reconcile
against — every other field defaults to zero rather than computing on unavailable data."""
from __future__ import annotations

from dataclasses import dataclass
from statistics import median
from typing import Protocol


@dataclass(frozen=True, slots=True)
class ReconResult:
    has_exec: bool
    cum_exec_return: float = 0.0
    cum_sim_return: float = 0.0
    divergence: float = 0.0          # cum_exec_return - cum_sim_return
    median_slippage_bps: float = 0.0
    total_fees: float = 0.0


class _ExecFillsSource(Protocol):
    """Matches `IbkrAccountRepo.fills_for` — the strategy-tagged `exec_fills` reads."""
    def fills_for(self, strategy_id: str, limit: int = 20) -> list: ...


class _ForwardCumSource(Protocol):
    """Matches `ForwardNavRepo.cum_return` — the persisted forward_summary cumulative return for a sid."""
    def cum_return(self, strategy_id: str) -> float | None: ...


def exec_vs_sim(strategy_id: str, *, exec_repo: _ExecFillsSource,
                forward_repo: _ForwardCumSource) -> ReconResult:
    """`strategy_id` is the MODELED sid (e.g. "multistrat"); its executed twin is `f"{strategy_id}_exec"` by
    the cockpit's existing naming convention (mirrors `CockpitBacktestRefProvider._REF_ALIAS`'s modeled<->exec
    pairing). Reads (not recomputes) both cumulative returns, and the fills for the honest cost signal."""
    exec_cum = forward_repo.cum_return(f"{strategy_id}_exec")
    if exec_cum is None:
        return ReconResult(has_exec=False)
    sim_cum = forward_repo.cum_return(strategy_id) or 0.0
    fills = exec_repo.fills_for(strategy_id, limit=2000)
    bps = [f.shortfall_bps for f in fills]
    return ReconResult(
        has_exec=True,
        cum_exec_return=exec_cum,
        cum_sim_return=sim_cum,
        divergence=exec_cum - sim_cum,
        median_slippage_bps=median(bps) if bps else 0.0,
        total_fees=sum(f.fee for f in fills),
    )
