"""Automated market making pipeline for hz.run().

Provides an Avellaneda-Stoikov style market maker with inventory skew,
competitive spread blending, and multi-level quoting.

Usage:
    hz.run(
        pipeline=[hz.market_maker(base_spread=0.04, gamma=0.3)],
        ...
    )
"""

from __future__ import annotations

from collections import deque
from typing import Callable

from horizon._horizon import (
    Quote,
    competitive_spread,
    estimate_volatility,
    mm_size,
    optimal_spread,
    reservation_price,
)
from horizon.context import Context


def market_maker(
    base_spread: float = 0.04,
    gamma: float = 0.5,
    kappa: float = 1.5,
    max_position: float = 100.0,
    num_levels: int = 1,
    level_spacing: float = 0.01,
    aggression: float = 0.5,
    volatility_window: int = 50,
    feed_name: str | None = None,
    size: float = 5.0,
    skew_factor: float = 0.5,
    time_horizon: float = 1.0,
) -> Callable[[Context], list[Quote]]:
    """Create a pipeline function that generates market making quotes.

    Uses Avellaneda-Stoikov reservation price and optimal spread,
    blended with observed market spread, with inventory-skewed sizing.

    Args:
        base_spread: Fallback spread when model spread unavailable.
        gamma: Risk aversion (higher = more aggressive inventory skew).
        kappa: Order arrival intensity (higher = tighter spreads).
        max_position: Maximum allowed inventory.
        num_levels: Number of quote levels (1 = single level).
        level_spacing: Price spacing between levels.
        aggression: 0.0 = model spread, 1.0 = book spread.
        volatility_window: Number of prices for rolling volatility.
        feed_name: Feed to read prices from (None = first available).
        size: Base order size per level.
        skew_factor: How aggressively to skew bid/ask sizes (0-1).
        time_horizon: Time horizon for spread/reservation calc.

    Returns:
        Pipeline function: (Context) -> list[Quote]
    """
    price_history: deque[float] = deque(maxlen=volatility_window)
    fill_count = 0
    quote_count = 0

    def _market_maker(ctx: Context, signal_value: float | None = None) -> list[Quote]:
        nonlocal fill_count, quote_count

        # 1. Get mid price from feed
        mid = 0.0
        book_spread_val = 0.0
        book_imbalance = 0.0

        if feed_name and feed_name in ctx.feeds:
            fd = ctx.feeds[feed_name]
        else:
            # Use first available feed
            fd = next(iter(ctx.feeds.values()), None)

        if fd is None:
            return []

        if fd.bid > 0 and fd.ask > 0:
            mid = (fd.bid + fd.ask) / 2.0
            book_spread_val = fd.ask - fd.bid
        elif fd.price > 0:
            mid = fd.price
        else:
            return []

        # 2. Get inventory
        market_id = ctx.market.id if ctx.market else ""

        # Increment fill_count from fills_this_cycle for adaptive sizing
        fills_this_cycle = ctx.params.get("fills_this_cycle", {})
        if market_id and market_id in fills_this_cycle:
            fill_count += len(fills_this_cycle[market_id])

        # Read orderbook imbalance from L2 data if available
        ob_feed = feed_name or next(iter(ctx.feeds), None)
        ob = ctx.params.get("orderbooks", {}).get(ob_feed)
        if ob is not None:
            try:
                book_imbalance = ob.imbalance(5)
            except Exception:
                pass
        inventory = ctx.inventory.net_for_market(market_id) if market_id else ctx.inventory.net

        # 3. Track price history and estimate volatility
        price_history.append(mid)
        vol = 0.0
        if len(price_history) >= 3:
            vol = estimate_volatility(list(price_history))

        if vol <= 0:
            vol = base_spread / 4.0  # Fallback: derive from spread

        # 4. Compute reservation price (inventory-skewed fair value)
        # If a signal_value is provided from an upstream combiner, use it as the fair value
        fair = signal_value if signal_value is not None and 0 < signal_value < 1 else mid
        res_price = reservation_price(fair, inventory, gamma, vol, time_horizon)

        # 5. Compute optimal spread
        opt_spread = optimal_spread(vol, inventory, gamma, kappa, time_horizon)
        if opt_spread <= 0:
            opt_spread = base_spread

        # 6. Blend with market spread
        final_spread = competitive_spread(
            opt_spread, book_spread_val if book_spread_val > 0 else opt_spread,
            book_imbalance, aggression
        )

        # 7. Compute inventory-skewed sizes
        fill_rate = fill_count / max(quote_count, 1)
        bid_size, ask_size = mm_size(size, inventory, max_position, fill_rate, skew_factor)

        # 8. Generate quotes for multiple levels
        quotes = []
        for level in range(num_levels):
            offset = level * level_spacing
            level_spread = final_spread + offset * 2

            bid_price = res_price - level_spread / 2.0
            ask_price = res_price + level_spread / 2.0

            # Size decay per level (0.8x per level)
            level_bid = bid_size * (0.8 ** level)
            level_ask = ask_size * (0.8 ** level)

            # Clamp to [0.01, 0.99] for prediction markets
            bid_price = max(0.01, min(0.99, bid_price))
            ask_price = max(0.01, min(0.99, ask_price))

            # Skip crossed quotes
            if bid_price >= ask_price:
                continue

            # Skip if sizes are too small
            if level_bid < 0.01 and level_ask < 0.01:
                continue

            q_size = max(level_bid, level_ask)
            quotes.append(Quote(bid=bid_price, ask=ask_price, size=q_size))

        quote_count += len(quotes)
        return quotes

    _market_maker.__name__ = "market_maker"
    return _market_maker
