"""Execution quality tracker for hz.run().

Monitors fill rate, slippage, and adverse selection to provide
real-time execution quality metrics.

Usage:
    hz.run(
        pipeline=[
            hz.execution_tracker(window=200),
            model,
            quoter,
        ],
        ...
    )
"""

from __future__ import annotations

from collections import deque
from dataclasses import dataclass
from typing import Callable

from horizon.context import Context


@dataclass
class ExecQuality:
    """Execution quality metrics."""

    fill_rate: float = 0.0
    avg_slippage: float = 0.0
    adverse_selection: float = 0.0
    total_fills: int = 0
    total_quotes: int = 0


def execution_tracker(
    window: int = 200,
    slippage_lookback: int = 50,
) -> Callable[[Context], None]:
    """Create an execution quality tracker.

    Reads fills from ``ctx.params["_recent_fills"]`` (injected by strategy.py)
    and computes fill rate, slippage, and adverse selection.

    Injects ``ctx.params["exec_quality"]`` with an ExecQuality dataclass.

    Args:
        window: Rolling window for fill rate calculation.
        slippage_lookback: Window for slippage/adverse selection metrics.

    Returns:
        Pipeline function: (Context) -> None
    """
    # Per-market state
    _seen_fills: dict[str, set[str]] = {}
    _seen_evict: dict[str, deque[str]] = {}
    _quote_cycles: dict[str, deque[bool]] = {}
    _fill_cycles: dict[str, deque[bool]] = {}
    _slippage_history: dict[str, deque[float]] = {}
    _adverse_history: dict[str, deque[float]] = {}
    _total_fills: dict[str, int] = {}
    _total_quotes: dict[str, int] = {}
    _last_mid: dict[str, float] = {}

    def _track(ctx: Context) -> None:
        market_id = ctx.market.id if ctx.market else "__default__"

        # Initialize per-market state
        if market_id not in _seen_fills:
            _seen_fills[market_id] = set()
            _seen_evict[market_id] = deque()
            _quote_cycles[market_id] = deque(maxlen=window)
            _fill_cycles[market_id] = deque(maxlen=window)
            _slippage_history[market_id] = deque(maxlen=slippage_lookback)
            _adverse_history[market_id] = deque(maxlen=slippage_lookback)
            _total_fills[market_id] = 0
            _total_quotes[market_id] = 0

        # Get current mid price
        mid = 0.0
        for fd in ctx.feeds.values():
            if fd.bid > 0 and fd.ask > 0:
                mid = (fd.bid + fd.ask) / 2.0
                break
            if fd.price > 0:
                mid = fd.price
                break

        # Count this as a quote cycle
        _quote_cycles[market_id].append(True)
        _total_quotes[market_id] += 1

        # Process new fills
        recent_fills = ctx.params.get("_recent_fills", [])
        had_fill = False
        for fill in recent_fills:
            fill_market = getattr(fill, "market_id", None)
            if fill_market != market_id:
                continue

            fill_id = getattr(fill, "fill_id", None)
            if fill_id is None:
                continue

            if fill_id in _seen_fills[market_id]:
                continue

            # Dedup: add to seen set with FIFO eviction
            _seen_fills[market_id].add(fill_id)
            _seen_evict[market_id].append(fill_id)
            if len(_seen_evict[market_id]) > 10000:
                old_id = _seen_evict[market_id].popleft()
                _seen_fills[market_id].discard(old_id)

            had_fill = True
            _total_fills[market_id] += 1

            # Slippage: distance from mid at time of fill
            fill_price = getattr(fill, "price", 0.0)
            prev_mid = _last_mid.get(market_id, mid)
            if prev_mid > 0:
                slippage = abs(fill_price - prev_mid)
                _slippage_history[market_id].append(slippage)

            # Adverse selection: did the mid move against us after the fill?
            # Compare current mid vs fill price
            if mid > 0 and fill_price > 0:
                order_side = getattr(fill, "order_side", None)
                if order_side is not None:
                    # Buy fill: adverse if mid dropped; Sell fill: adverse if mid rose
                    from horizon._horizon import OrderSide

                    if order_side == OrderSide.Buy:
                        adverse = mid - fill_price  # Negative = adverse
                    else:
                        adverse = fill_price - mid  # Negative = adverse
                    _adverse_history[market_id].append(adverse)

        _fill_cycles[market_id].append(had_fill)
        _last_mid[market_id] = mid

        # Compute metrics
        fills_window = _fill_cycles[market_id]
        quote_count = len(_quote_cycles[market_id])
        fill_count = sum(1 for f in fills_window if f)
        fill_rate = fill_count / max(quote_count, 1)

        slippage_vals = list(_slippage_history[market_id])
        avg_slippage = sum(slippage_vals) / len(slippage_vals) if slippage_vals else 0.0

        adverse_vals = list(_adverse_history[market_id])
        adverse_selection = sum(adverse_vals) / len(adverse_vals) if adverse_vals else 0.0

        ctx.params["exec_quality"] = ExecQuality(
            fill_rate=fill_rate,
            avg_slippage=avg_slippage,
            adverse_selection=adverse_selection,
            total_fills=_total_fills[market_id],
            total_quotes=_total_quotes[market_id],
        )

    _track.__name__ = "execution_tracker"
    return _track
