"""
Composite Market Making — weighted ensemble of OBI, A-S, and Hurst/VPIN.

Each sub-strategy votes: +1 (long), -1 (short), 0 (neutral).
Weighted score > entry_threshold → enter. Score crosses below exit_threshold → exit.

Weights (configurable):
  - OBI (30%): volume-based order book imbalance
  - A-S (40%): inventory risk aversion — net short → buy bias, net long → sell bias
  - Hurst/VPIN (30%): trending regime + informed flow direction

Entry: |weighted_score| > 0.5
Exit:  |weighted_score| < 0.3
Stop-loss: 2%, take-profit: 2x fee, cooldown: 3 bars
"""
from __future__ import annotations

import logging
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 CompositeMMNT(BaseHlStrategy):
    """Weighted ensemble of multiple signal sources for market making."""

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

        # Weights (must sum to 1.0 for easy interpretation)
        self._w_obi = config.params.get("obi_weight", 0.30)
        self._w_as = config.params.get("as_weight", 0.40)
        self._w_hurst = config.params.get("hurst_weight", 0.30)
        self._entry_score = config.params.get("entry_score", 0.50)
        self._exit_score = config.params.get("exit_score", 0.30)
        self._stop_loss_pct = config.params.get("stop_loss_pct", 0.02)
        self._take_profit_pct = config.params.get("take_profit_pct", 0.005)
        self._cooldown_bars = config.params.get("cooldown_bars", 3)

        # Sub-strategy instances (lazy)
        self._obi = None
        self._as_mm = None
        self._hurst = None

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

    # ── Lazy sub-strategy init ──────────────────────────────────

    def _init_obi(self):
        if self._obi is None:
            from strategies.nt.obi_nt import OBINT
            obi_cfg = StrategyConfig(
                name="OBI-sub", asset=self._cfg.asset,
                instrument=self._cfg.instrument, allocation=self._cfg.allocation,
                order_size=self._cfg.order_size, fee_model="taker",
                params={"obi_lookback": 20, "obi_entry": 0.30, "obi_exit": 0.10, "cooldown_bars": 0},
            )
            self._obi = OBINT(obi_cfg)

    def _init_as(self):
        if self._as_mm is None:
            from strategies.nt.as_mm_nt import ASMarketMakingNT
            as_cfg = StrategyConfig(
                name="AS-sub", asset=self._cfg.asset,
                instrument=self._cfg.instrument, allocation=self._cfg.allocation,
                order_size=self._cfg.order_size, fee_model="maker",
                params={"gamma": 0.1, "max_inventory": self._cfg.order_size * 10},
            )
            self._as_mm = ASMarketMakingNT(as_cfg)

    def _init_hurst(self):
        if self._hurst is None:
            from strategies.nt.hurst_vpin_nt import HurstVPINNT
            hv_cfg = StrategyConfig(
                name="HV-sub", asset=self._cfg.asset,
                instrument=self._cfg.instrument, allocation=self._cfg.allocation,
                order_size=self._cfg.order_size, fee_model="taker",
                params={"hurst_window": 64, "hurst_entry": 0.55,
                        "vpin_threshold": 0.25, "dollar_threshold": 100000.0},
            )
            self._hurst = HurstVPINNT(hv_cfg)

    # ── Bar handler ─────────────────────────────────────────────

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

        # Feed all sub-strategies
        self._init_obi()
        self._init_as()
        self._init_hurst()

        # Feed bar to sub-strategies (they accumulate state internally)
        self._obi.on_bar(bar)
        self._as_mm.on_bar(bar)
        self._hurst.on_bar(bar)

        self._bars_since_trade += 1

        # Exit check
        if self._in_trade:
            if self._check_exit(price):
                return
            return

        if self._bars_since_trade < self._cooldown_bars:
            return

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

    # ── Ensemble computation ────────────────────────────────────

    def _compute_ensemble(self) -> dict | None:
        # OBI vote
        obi_vote = 0.0
        obi_sig = self._obi._compute_obi_signal()
        if obi_sig:
            obi_vote = 1.0 if "BUY" in obi_sig["signal"] else -1.0

        # A-S vote: inventory skew = -sign(inventory)
        as_vote = 0.0
        as_inventory = self._as_mm._inventory
        max_inv = self._as_mm._max_inventory
        if max_inv > 0:
            as_vote = -as_inventory / max_inv  # +1 when deeply short, -1 when deeply long

        # Hurst vote
        hurst_vote = 0.0
        hv_sig = self._hurst._compute_hurst_vpin_signal()
        if hv_sig:
            hurst_vote = 1.0 if "BUY" in hv_sig["signal"] else -1.0

        score = self._w_obi * obi_vote + self._w_as * as_vote + self._w_hurst * hurst_vote

        if abs(score) >= self._entry_score:
            self._in_trade = True
            self._trade_direction = "long" if score > 0 else "short"
            self._entry_price = self._prices[-1] if self._prices else 0.0
            self._bars_since_trade = 0

            return {
                "signal": "BUY" if score > 0 else "SELL",
                "strength": abs(score) / self._entry_score,
                "score": round(score, 3),
                "votes": f"obi={obi_vote:.1f}_as={as_vote:.2f}_hurst={hurst_vote:.1f}",
                "reason": "composite_ensemble",
            }

        return None

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

    def _check_exit(self, current_price: float) -> bool:
        if not self._in_trade or self._entry_price <= 0:
            return False

        change_pct = (current_price - self._entry_price) / self._entry_price
        pnl_pct = change_pct if self._trade_direction == "long" else -change_pct

        exit_reason = None

        if pnl_pct <= -self._stop_loss_pct:
            exit_reason = "stop_loss"
        elif pnl_pct >= self._take_profit_pct:
            exit_reason = "take_profit"
        elif abs(self._weighted_score_fast()) < self._exit_score:
            exit_reason = "score_reverted"

        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

    def _weighted_score_fast(self) -> float:
        """Fast ensemble score (no sub-signal computation, just state)."""
        as_inv = self._as_mm._inventory
        max_inv = self._as_mm._max_inventory
        as_vote = -as_inv / max_inv if max_inv > 0 else 0.0

        obi_list = list(self._obi._buy_volumes) if self._obi and self._obi._buy_volumes else []
        sell_list = list(self._obi._sell_volumes) if self._obi and self._obi._sell_volumes else []
        obi_vote = 0.0
        total_buy = sum(obi_list[-10:]) if obi_list else 0
        total_sell = sum(sell_list[-10:]) if sell_list else 0
        total = total_buy + total_sell
        if total > 0:
            obi_vote = (total_buy - total_sell) / total

        return self._w_obi * obi_vote + self._w_as * as_vote

    # ── Signal (for paper trader) ───────────────────────────────

    def compute_signal(self, price: float | None = None,
                       orderbook: dict | None = None) -> dict | None:
        if price is None or price <= 0:
            return None
        self._prices.append(price)
        self._init_obi()
        self._init_as()
        self._init_hurst()

        # Feed price to sub-strategies
        if price > 0:
            self._obi.compute_signal(price=price)
            self._as_mm.compute_signal(price=price)
            self._hurst.compute_signal(price=price)

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

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

        return self._compute_ensemble()

    # ── 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)
