"""Adaptive spread pipeline modifier for hz.run().

Dynamically scales spread based on fill rate, volatility, and orderbook
imbalance. Widens when conditions are adverse, tightens when calm.

Usage:
    hz.run(
        pipeline=[model, quoter, hz.adaptive_spread()],
        ...
    )
"""

from __future__ import annotations

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

from horizon._horizon import Quote, estimate_volatility
from horizon.context import Context


@dataclass
class SpreadMetrics:
    """Metrics computed by the adaptive spread modifier."""

    fill_rate: float = 0.0
    realized_vol: float = 0.0
    imbalance: float = 0.0
    spread_multiplier: float = 1.0


def adaptive_spread(
    fill_rate_window: int = 100,
    vol_window: int = 50,
    fill_rate_target: float = 0.3,
    vol_sensitivity: float = 2.0,
    imbalance_sensitivity: float = 0.5,
    min_multiplier: float = 0.5,
    max_multiplier: float = 3.0,
) -> Callable[[Context, list[Quote]], list[Quote]]:
    """Create an adaptive spread modifier.

    Scales the bid-ask spread based on market conditions:
    - High fill rate → widen spread (adverse selection)
    - High volatility → widen spread (risk)
    - High imbalance → widen spread (one-sided flow)

    Args:
        fill_rate_window: Window for fill rate calculation.
        vol_window: Window for volatility estimation.
        fill_rate_target: Target fill rate (above this → widen).
        vol_sensitivity: Sensitivity to volatility changes.
        imbalance_sensitivity: Sensitivity to orderbook imbalance.
        min_multiplier: Floor on spread multiplier.
        max_multiplier: Ceiling on spread multiplier.

    Returns:
        Pipeline function: (Context, list[Quote]) -> list[Quote]
    """
    # Per-market state
    _fill_history: dict[str, deque[bool]] = {}
    _price_history: dict[str, deque[float]] = {}
    _prev_inventory: dict[str, float] = {}

    def _adapt(ctx: Context, quotes: list[Quote] | None) -> list[Quote]:
        if not quotes:
            return []

        market_id = ctx.market.id if ctx.market else "__default__"

        # Initialize per-market state
        if market_id not in _fill_history:
            _fill_history[market_id] = deque(maxlen=fill_rate_window)
        if market_id not in _price_history:
            _price_history[market_id] = deque(maxlen=vol_window)

        # Get 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

        if mid > 0:
            _price_history[market_id].append(mid)

        # Detect fills: check ctx.params or use inventory change heuristic
        fills_this_cycle = ctx.params.get("fills_this_cycle", {})
        had_fill = bool(fills_this_cycle.get(market_id))
        if not had_fill:
            current_inv = (
                ctx.inventory.net_for_market(market_id)
                if ctx.market
                else ctx.inventory.net
            )
            prev_inv = _prev_inventory.get(market_id, current_inv)
            had_fill = abs(current_inv - prev_inv) > 1e-10
            _prev_inventory[market_id] = current_inv

        _fill_history[market_id].append(had_fill)

        # Compute fill rate
        fills = _fill_history[market_id]
        fill_rate = sum(1 for f in fills if f) / max(len(fills), 1)

        # Compute volatility
        prices = list(_price_history[market_id])
        vol = estimate_volatility(prices) if len(prices) >= 3 else 0.0

        # Compute imbalance from feeds
        imbalance = 0.0
        for fd in ctx.feeds.values():
            if fd.bid > 0 and fd.ask > 0:
                local_mid = (fd.bid + fd.ask) / 2.0
                if local_mid > 0:
                    imbalance = (fd.bid - local_mid) / local_mid
                break

        # Spread multiplier components
        fill_component = 1.0 + max(0.0, (fill_rate - fill_rate_target) / fill_rate_target) if fill_rate_target > 0 else 1.0
        vol_component = 1.0 + vol * vol_sensitivity
        imbalance_component = 1.0 + abs(imbalance) * imbalance_sensitivity

        multiplier = fill_component * vol_component * imbalance_component
        multiplier = max(min_multiplier, min(max_multiplier, multiplier))

        # Inject metrics
        ctx.params["spread_metrics"] = SpreadMetrics(
            fill_rate=fill_rate,
            realized_vol=vol,
            imbalance=imbalance,
            spread_multiplier=multiplier,
        )

        # Apply multiplier to spread
        result = []
        for q in quotes:
            q_mid = (q.bid + q.ask) / 2.0
            q_spread = q.ask - q.bid
            new_spread = q_spread * multiplier
            new_bid = q_mid - new_spread / 2.0
            new_ask = q_mid + new_spread / 2.0

            # Clamp to prediction market range
            new_bid = max(0.01, min(0.99, new_bid))
            new_ask = max(0.01, min(0.99, new_ask))

            if new_bid >= new_ask:
                mid_val = (new_bid + new_ask) / 2.0
                new_bid = max(0.01, mid_val - 0.005)
                new_ask = min(0.99, mid_val + 0.005)
                if new_bid >= new_ask:
                    continue

            result.append(Quote(bid=new_bid, ask=new_ask, size=q.size))

        return result

    _adapt.__name__ = "adaptive_spread"
    return _adapt
