"""Execution-gap verdict + indicative slippage (design §5) — the execution analog of the recon gate.

Pure functions over the recorded testnet rows (no I/O): the reconcile/record assets persist post-rebalance
positions and per-fill slippage to `bybit_testnet_nav` / `bybit_testnet_fills`; the read-models load them into
the small frozen DTOs below and these functions compute the verdict the cockpit (Phase 2) will surface.

  * `mechanics_verdict` — MECHANICS PASS iff, over the window, every recorded post-rebalance position tracks its
    effective target within `tolerance` (each symbol within ~1 qtyStep + dust) AND there are no reconcile-error
    / execution-gap rows. Otherwise FAIL, naming the offending symbol(s). A target with no recorded row is an
    execution gap and counts as offending.
  * `indicative_slippage` — aggregate `slippage_vs_assumed` (the `slippage_delta_bps`) per A8 cost tier,
    EXCLUDING `low_confidence` (thin-book / one-sided-mid) fills, and report `coverage` = real-liquidity fills /
    all fills in that tier. Explicitly indicative: magnitude is pending mainnet micro-live (spec §3, §11).
"""
from __future__ import annotations

from dataclasses import dataclass


@dataclass(frozen=True, slots=True)
class PositionRow:
    """A recorded post-rebalance position for one symbol on one run. `error=True` flags a reconcile error /
    execution gap for the symbol (a placement failure, a missing fill) — a visible gap, not a silent pass."""

    symbol: str
    position: float          # recorded post-rebalance signed base qty
    error: bool = False


@dataclass(frozen=True, slots=True)
class SlippageFill:
    """A recorded fill's slippage-vs-assumed for tier aggregation. `low_confidence=True` marks a fill whose mid
    came from an empty/one-sided thin testnet book (`mid` was `None`) — recorded but excluded from the verdict."""

    tier: str
    slippage_delta_bps: float
    low_confidence: bool = False


@dataclass(frozen=True, slots=True)
class Verdict:
    passed: bool
    offending: tuple[str, ...]


def mechanics_verdict(
    rows: list[PositionRow], target_positions: dict[str, float], tolerance: float
) -> Verdict:
    """PASS iff every effective target is tracked by a recorded post-rebalance position within `tolerance` and
    no row is an error/gap row; else FAIL with the sorted offending symbols. A target lacking a recorded row is
    an execution gap (offending)."""
    by_symbol = {r.symbol: r for r in rows}
    offending: set[str] = set()
    for row in rows:
        if row.error:
            offending.add(row.symbol)
    for symbol, target in target_positions.items():
        row = by_symbol.get(symbol)
        if row is None:                                  # intended a position, recorded nothing → gap
            offending.add(symbol)
            continue
        if abs(row.position - target) > tolerance:
            offending.add(symbol)
    ordered = tuple(sorted(offending))
    return Verdict(passed=not ordered, offending=ordered)


def indicative_slippage(fills: list[SlippageFill]) -> dict[str, dict[str, float | None]]:
    """`{tier: {"mean_delta_bps", "coverage"}}` — per-tier mean `slippage_delta_bps` over real-liquidity fills
    (low-confidence excluded), with `coverage` = real fills / all fills in the tier. A tier with only
    low-confidence fills has `mean_delta_bps=None` and `coverage=0.0`."""
    totals: dict[str, list[float]] = {}
    counts: dict[str, int] = {}
    for fill in fills:
        counts[fill.tier] = counts.get(fill.tier, 0) + 1
        if not fill.low_confidence:
            totals.setdefault(fill.tier, []).append(fill.slippage_delta_bps)
    out: dict[str, dict[str, float | None]] = {}
    for tier, n_total in counts.items():
        reals = totals.get(tier, [])
        mean = sum(reals) / len(reals) if reals else None
        out[tier] = {"mean_delta_bps": mean, "coverage": len(reals) / n_total}
    return out
