"""
Avellaneda-Stoikov Market Making NautilusTrader strategy.

Inventory-aware dual-sided quoting with stochastic control.
Adapted from the production ASMarketMaker (strategies/as_quoter.py).

Key insight: AS tells you WHEN to quote each side, not what price.
We always quote at best bid/ask — the AS formula controls which sides
are active based on inventory risk and reservation price.

When long → reservation price drops → stop quoting bid side
When short → reservation price rises → stop quoting ask side
When flat → quote both sides

For backtesting: simulate maker fills when price reaches our levels.
For live: submit POST-ONLY limit orders at best bid/ask.
"""
from __future__ import annotations

import logging
import math
from collections import deque

import numpy as np

from nautilus_trader.model.data import Bar
from nautilus_trader.model.enums import OrderSide

from framework.base_strategy import BaseHlStrategy
from framework.config import StrategyConfig

logger = logging.getLogger(__name__)


class ASMarketMakingNT(BaseHlStrategy):
    """A-S stochastic control market making — side selection, not price selection."""

    def __init__(self, config: StrategyConfig):
        super().__init__(config)

        # A-S parameters
        self._gamma = config.params.get("gamma", 0.1)
        self._tau = config.params.get("tau", 1.0)       # Session length (hours)
        self._max_inventory = config.params.get("max_inventory", config.order_size * 10)
        self._gamma_scale = config.params.get("gamma_scale", 500000)
        self._vol_window = config.params.get("vol_window", 300)

        # Vol estimation
        self._sigma_prices: deque[float] = deque(maxlen=self._vol_window)
        self._sigma: float = 0.01

        # Inventory tracking
        self._inventory: float = 0.0
        self._last_mid: float = 0.0
        self._tick_count: int = 0

        # Fill simulation (backtest mode)
        self._fills: list[dict] = []
        self._cumulative_pnl: float = 0.0

    def on_bar(self, bar: Bar):
        mid = float(bar.close)
        self._sigma_prices.append(mid)

        self._update_vol()
        self._tick_count += 1

        # Simulate bid/ask from candle high/low
        bid = float(bar.low)
        ask = float(bar.high)

        # T elapsed for this bar (approximate)
        t = (self._tick_count * 1.0) / (self._tau * 3600)  # Simplified

        selection = self._should_quote(mid, bid, ask, t)

        if not selection.get("quote_bid") and not selection.get("quote_ask"):
            return  # No quoting — circuit breaker active

        # Simulate fill: if we quoted bid and price went down past our level
        if selection.get("quote_bid"):
            # Check if candle low dipped below our bid level
            if float(bar.low) <= bid:
                self._simulate_fill(OrderSide.BUY, bid)

        if selection.get("quote_ask"):
            if float(bar.high) >= ask:
                self._simulate_fill(OrderSide.SELL, ask)

    def _update_vol(self):
        if len(self._sigma_prices) >= 10:
            prices = list(self._sigma_prices)
            returns = [(prices[i] - prices[i - 1]) / prices[i - 1] for i in range(1, len(prices))]
            mu = np.mean(returns)
            var = np.mean([(r - mu) ** 2 for r in returns])
            self._sigma = max(math.sqrt(var) if var > 0 else 0.01, 0.001)

    def _should_quote(self, mid: float, best_bid: float, best_ask: float, t: float) -> dict:
        # Hard inventory bounds
        if abs(self._inventory) >= self._max_inventory:
            if self._inventory > 0:
                return {"quote_bid": False, "quote_ask": True, "reservation": mid, "sigma": self._sigma}
            else:
                return {"quote_bid": True, "quote_ask": False, "reservation": mid, "sigma": self._sigma}

        # Circuit breaker: skip if vol is extremely high (> 3x normal)
        if len(self._sigma_prices) >= 5:
            recent = list(self._sigma_prices)[-5:]
            move_pct = abs(recent[-1] - recent[0]) / (recent[0] + 1e-8)
            if move_pct > 3 * self._sigma * math.sqrt(5):
                return {"quote_bid": False, "quote_ask": False, "reservation": mid, "sigma": self._sigma}

        # Reservation price from A-S formula
        q_notional = self._inventory * mid
        sigma_sq = max(self._sigma ** 2, 0.000001)
        reservation = mid - self._inventory * self._gamma * sigma_sq * self._tau

        # Quote sides based on reservation vs market
        quote_bid = reservation >= best_bid or abs(self._inventory) < self._max_inventory * 0.1
        quote_ask = reservation <= best_ask or abs(self._inventory) < self._max_inventory * 0.1

        return {
            "quote_bid": quote_bid,
            "quote_ask": quote_ask,
            "reservation": reservation,
            "sigma": self._sigma,
        }

    def _simulate_fill(self, side: OrderSide, price: float):
        """Simulate a fill in backtest mode."""
        size = self._cfg.order_size
        fee_rate = self._cfg.maker_fee if self._cfg.fee_model == "maker" else self._cfg.taker_fee
        fee = size * price * fee_rate

        # Update inventory + PnL
        if side == OrderSide.BUY:
            self._inventory += size
            # PnL from spread capture
            self._cumulative_pnl -= fee
        else:
            self._inventory -= size
            self._cumulative_pnl -= fee

        # Assume we close immediately at same price (simplification for backtest)
        # In production, fills are tracked by the real exchange
        self._fills.append({
            "side": "BUY" if side == OrderSide.BUY else "SELL",
            "size": size,
            "price": price,
            "fee": round(fee, 6),
            "inventory": round(self._inventory, 8),
            "cumulative_pnl": round(self._cumulative_pnl, 4),
        })

    def compute_signal(self, price: float | None = None) -> dict | None:
        """External signal compute for paper trade orchestrator."""
        if price is None or price <= 0:
            return None

        self._sigma_prices.append(price)
        self._update_vol()

        # Return quoting decision as a signal
        selection = self._should_quote(price, price * 0.999, price * 1.001, 0.5)

        if selection.get("quote_bid") and selection.get("quote_ask"):
            return {"signal": "DUAL", "strength": 1.0, "reservation": selection.get("reservation", price)}
        elif selection.get("quote_bid"):
            return {"signal": "BID_ONLY", "strength": 1.0, "reservation": selection.get("reservation", price)}
        elif selection.get("quote_ask"):
            return {"signal": "ASK_ONLY", "strength": 1.0, "reservation": selection.get("reservation", price)}
        return None

    def handle_signal(self, signal: dict):
        sig = signal.get("signal", "")
        if "DUAL" in sig:
            self._submit_order(OrderSide.BUY)
            self._submit_order(OrderSide.SELL)
        elif "BID" in sig:
            self._submit_order(OrderSide.BUY)
        elif "ASK" in sig:
            self._submit_order(OrderSide.SELL)
