"""
Order Book Imbalance NautilusTrader strategy.

Trades on L2 bid/ask volume skew. When bids dominate, price tends to
rise as heavy bid side absorbs sell market orders. When asks dominate,
price tends to fall.

Dual-mode operation:
  - Backtest:  volume-based OBI proxy from candle OHLCV
               (buy_vol if close > open else sell_vol, rolling window)
  - Live/Paper: real L2 orderbook from HyperliquidDataProvider.fetch_orderbook()
               (bid_vol / total_vol at top N levels)

Strategy logic:
  1. Compute imbalance over lookback window
  2. Signal when |imbalance| > entry threshold
  3. Exit on reversion, stop-loss, or take-profit
  4. Cooldown bars between signals to avoid overtrading
"""
from __future__ import annotations

import logging
from collections import deque
from typing import Any

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 OBINT(BaseHlStrategy):
    """Order Book Imbalance — volume skew mean-reversion / momentum."""

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

        # OBI params
        self._obi_lookback = config.params.get("obi_lookback", 20)
        self._obi_entry = config.params.get("obi_entry", 0.35)       # |imbalance| > this → enter
        self._obi_exit = config.params.get("obi_exit", 0.10)         # |imbalance| < this → exit
        self._obi_depth = config.params.get("obi_depth", 10)         # L2 depth levels (live mode)
        self._cooldown_bars = config.params.get("cooldown_bars", 3)
        self._stop_loss_pct = config.params.get("stop_loss_pct", 0.02)
        self._take_profit_pct = config.params.get("take_profit_pct", 0.005)

        # Volume tracking for candle-based OBI proxy
        self._buy_volumes: deque[float] = deque(maxlen=self._obi_lookback)
        self._sell_volumes: deque[float] = deque(maxlen=self._obi_lookback)

        # State
        self._bars_since_trade = self._cooldown_bars  # start ready
        self._in_trade = False
        self._trade_direction: str | None = None
        self._entry_price: float = 0.0

    # ── Bar handler (backtest mode — candle proxy) ─────────────

    def on_bar(self, bar: Bar):
        price = float(bar.close)
        self._prices.append(price)

        # Classify volume: buy if close > open, sell if close < open
        close_px = float(bar.close)
        open_px = float(bar.open)
        volume = float(bar.volume) if hasattr(bar, 'volume') else 1.0

        if close_px > open_px:
            self._buy_volumes.append(volume)
            self._sell_volumes.append(0.0)
        elif close_px < open_px:
            self._buy_volumes.append(0.0)
            self._sell_volumes.append(volume)
        else:
            # Flat bar — split evenly
            self._buy_volumes.append(volume * 0.5)
            self._sell_volumes.append(volume * 0.5)

        self._bars_since_trade += 1

        # Check exits first if in trade
        if self._in_trade:
            if self._check_exit(price):
                return
            return

        # Cooldown check
        if self._bars_since_trade < self._cooldown_bars:
            return

        # Compute signal
        signal = self._compute_obi_signal()
        if signal:
            self._last_signal = signal
            self.handle_signal(signal)

    # ── Live/paper mode — real L2 orderbook ────────────────────

    def compute_signal(self, price: float | None = None,
                       orderbook: dict | None = None) -> dict | None:
        """Entry point for paper trader / deploy orchestrator.

        If orderbook is provided, use real L2 OBI.
        Otherwise fall back to candle proxy with price feed.
        """
        if orderbook is not None:
            return self._compute_l2_signal(orderbook)

        if price is None:
            return None

        self._prices.append(price)

        if self._in_trade:
            if self._check_exit(price):
                return self._last_signal

        if self._bars_since_trade < self._cooldown_bars:
            self._bars_since_trade += 1
            return None

        return self._compute_obi_signal()

    def _compute_l2_signal(self, orderbook: dict) -> dict | None:
        """Compute OBI from real L2 orderbook snapshot."""
        bids = orderbook.get("bids", [])
        asks = orderbook.get("asks", [])
        if not bids or not asks:
            return None

        depth = min(self._obi_depth, len(bids), len(asks))
        bid_vol = sum(bids[i][1] for i in range(depth))
        ask_vol = sum(asks[i][1] for i in range(depth))
        total = bid_vol + ask_vol
        if total <= 0:
            return None

        obi = bid_vol / total  # 0-1: > 0.5 = bids heavier, < 0.5 = asks heavier

        # Convert to signed imbalance (-1 to +1)
        imbalance = (obi - 0.5) * 2

        # Entry
        if not self._in_trade:
            if imbalance > self._obi_entry:
                self._in_trade = True
                self._trade_direction = "long"
                self._entry_price = bids[0][0] if bids else 0
                self._bars_since_trade = 0
                return {"signal": "BUY", "strength": imbalance / self._obi_entry,
                        "obi": round(obi, 3), "reason": "l2_bid_heavy"}

            if imbalance < -self._obi_entry:
                self._in_trade = True
                self._trade_direction = "short"
                self._entry_price = asks[0][0] if asks else 0
                self._bars_since_trade = 0
                return {"signal": "SELL", "strength": abs(imbalance) / self._obi_entry,
                        "obi": round(obi, 3), "reason": "l2_ask_heavy"}

        # Exit — imbalance reverted
        elif abs(imbalance) < self._obi_exit:
            exit_signal = "SELL" if self._trade_direction == "long" else "BUY"
            self._in_trade = False
            self._trade_direction = None
            return {"signal": exit_signal, "strength": 0.0,
                    "reason": "l2_imbalance_exit"}

        return None

    # ── Candle-based OBI (backtest proxy) ──────────────────────

    def _compute_obi_signal(self) -> dict | None:
        if len(self._buy_volumes) < self._obi_lookback:
            return None

        buy_vol_list = list(self._buy_volumes)
        sell_vol_list = list(self._sell_volumes)
        total_buy = sum(buy_vol_list)
        total_sell = sum(sell_vol_list)
        total_vol = total_buy + total_sell

        if total_vol <= 0:
            return None

        # Signed imbalance: +1 = all buy, -1 = all sell
        imbalance = (total_buy - total_sell) / total_vol

        if imbalance > self._obi_entry:
            self._in_trade = True
            self._trade_direction = "long"
            self._entry_price = self._prices[-1] if self._prices else 0
            self._bars_since_trade = 0
            return {
                "signal": "BUY",
                "strength": imbalance / self._obi_entry,
                "imbalance": round(imbalance, 3),
                "reason": "candle_bid_heavy",
            }

        if imbalance < -self._obi_entry:
            self._in_trade = True
            self._trade_direction = "short"
            self._entry_price = self._prices[-1] if self._prices else 0
            self._bars_since_trade = 0
            return {
                "signal": "SELL",
                "strength": abs(imbalance) / self._obi_entry,
                "imbalance": round(imbalance, 3),
                "reason": "candle_ask_heavy",
            }

        return None

    # ── Exit logic ─────────────────────────────────────────────

    def _check_exit(self, current_price: float) -> bool:
        """Check exit conditions. Returns True if an exit signal was generated."""
        if not self._in_trade or self._entry_price <= 0:
            return False

        change_pct = (current_price - self._entry_price) / self._entry_price

        if self._trade_direction == "long":
            pnl_pct = change_pct
        else:
            pnl_pct = -change_pct

        exit_reason = None

        # Stop loss
        if pnl_pct <= -self._stop_loss_pct:
            exit_reason = "stop_loss"
        # Take profit
        elif pnl_pct >= self._take_profit_pct:
            exit_reason = "take_profit"
        # Imbalance reversion (check candle proxy)
        elif len(self._buy_volumes) >= self._obi_lookback:
            total_buy = sum(self._buy_volumes)
            total_sell = sum(self._sell_volumes)
            total_vol = total_buy + total_sell
            if total_vol > 0:
                imbalance = (total_buy - total_sell) / total_vol
                if (self._trade_direction == "long" and imbalance < -self._obi_exit) or \
                   (self._trade_direction == "short" and imbalance > self._obi_exit):
                    exit_reason = "imbalance_flip"

        if exit_reason is None:
            return False

        exit_side = "SELL" if self._trade_direction == "long" else "BUY"
        self._last_signal = {
            "signal": exit_side,
            "strength": abs(pnl_pct) / self._stop_loss_pct,
            "pnl_pct": round(pnl_pct * 100, 2),
            "reason": exit_reason,
        }
        self._in_trade = False
        self._trade_direction = None
        self.handle_signal(self._last_signal)
        return True

    # ── Order ──────────────────────────────────────────────────

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