"""
NautilusTrader event-driven backtest engine for Hyperliquid strategies.

Sets up a BacktestEngine with Hyperliquid venue, instruments, historical
bar data, and registered strategies. Runs event-driven simulation with
realistic fill emulation (maker/taker, slippage).

Slower but more realistic than VectorBT — intended for final validation
before paper/live deployment.
"""
from __future__ import annotations

import logging
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

import numpy as np
import pandas as pd

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from nautilus_trader.backtest.engine import BacktestEngine, BacktestEngineConfig
from nautilus_trader.model.data import Bar, BarSpecification, BarType
from nautilus_trader.model.enums import AccountType, BarAggregation, OmsType, PriceType
from nautilus_trader.model.identifiers import InstrumentId, Venue
from nautilus_trader.model.instruments import CryptoPerpetual
from nautilus_trader.model.objects import Currency, Money, Price, Quantity

from framework.data import HyperliquidDataProvider, INTERVAL_TO_SECONDS
from framework.instruments import HL_VENUE

logger = logging.getLogger(__name__)


INTERVAL_TO_AGG = {
    "1m": (1, BarAggregation.MINUTE),
    "5m": (5, BarAggregation.MINUTE),
    "15m": (15, BarAggregation.MINUTE),
    "30m": (30, BarAggregation.MINUTE),
    "1h": (1, BarAggregation.HOUR),
    "4h": (4, BarAggregation.HOUR),
    "8h": (8, BarAggregation.HOUR),
    "1d": (1, BarAggregation.DAY),
}


class NTBacktestRunner:
    """NautilusTrader backtest engine wrapper for Hyperliquid."""

    def __init__(self):
        pass

    def run_backtest(
        self,
        strategy: str = "pairs",
        interval: str = "1h",
        instruments: dict[str, CryptoPerpetual] | None = None,
        testnet: bool = False,
        limit: int = 5000,
    ) -> dict[str, Any] | None:
        """Run event-driven backtest with NautilusTrader.

        1. Set up BacktestEngine
        2. Register Hyperliquid venue + instruments
        3. Load historical bars from Hyperliquid
        4. Add strategy and run
        5. Return metrics
        """

        step, agg = INTERVAL_TO_AGG.get(interval, (1, BarAggregation.HOUR))

        config = BacktestEngineConfig()
        engine = BacktestEngine(config=config)
        engine.add_venue(
            venue=HL_VENUE,
            oms_type=OmsType.NETTING,
            account_type=AccountType.MARGIN,
            starting_balances=[Money(10_000.0, Currency.from_str("USD"))],
        )

        # Add instruments
        coin = self._get_coin(strategy)
        inst_for_coin = None
        if instruments:
            for name, inst in instruments.items():
                engine.add_instrument(inst)
                if name.upper() == coin.upper():
                    inst_for_coin = inst

        if not inst_for_coin and instruments:
            # Try to find any instrument matching
            for inst in instruments.values():
                instr_name = str(inst.id.symbol)
                if coin.upper() in instr_name.upper():
                    inst_for_coin = inst
                    break

        sz_prec = inst_for_coin.size_precision if inst_for_coin else 5

        # Fetch real candles
        provider = HyperliquidDataProvider(testnet=testnet)
        df = provider.fetch_candles(coin, interval=interval, limit=limit)
        if df.empty:
            logger.error("No candles for %s", coin)
            return None

        # Build bars
        inst_id = InstrumentId.from_str(f"{coin.upper()}-USD-PERP.HYPERLIQUID")
        bars = self._df_to_bars(df, inst_id, step, agg, size_precision=sz_prec)

        # Add bars
        engine.add_data(bars)

        # Add strategy
        strategy_class = self._resolve_strategy_class(strategy)
        if strategy_class is None:
            logger.error("No NT strategy class for %s", strategy)
            return None

        from framework.config import StrategyConfig
        cfg = StrategyConfig(
            name=strategy,
            instrument=f"{coin}-USD-PERP",
            asset=coin,
            allocation=10000.0,
            order_size=0.001,
        )
        nt_strategy = strategy_class(cfg)
        engine.add_strategy(nt_strategy)

        # Run
        try:
            result = engine.run()
        except Exception as e:
            logger.error("Backtest engine error: %s", e)
            import traceback
            traceback.print_exc()
            return None

        # Extract metrics
        return self._extract_result(result, engine, strategy, interval, len(bars))

    # ── Helpers ─────────────────────────────────────────────────

    def _get_coin(self, strategy: str) -> str:
        return {"pairs": "ETH", "hurst_vpin": "BTC", "as_mm": "BTC",
                "obi": "BTC", "funding_arb": "BTC"}.get(strategy, "BTC")

    def _df_to_bars(
        self,
        df: pd.DataFrame,
        instrument_id: InstrumentId,
        step: int,
        aggregation: BarAggregation,
        size_precision: int = 5,
    ) -> list[Bar]:
        spec = BarSpecification(step, aggregation, PriceType.LAST)
        bar_type = BarType(instrument_id, spec)
        bars = []
        for idx, row in df.iterrows():
            ts = int(idx.timestamp() * 1e9)
            bar = Bar(
                bar_type=bar_type,
                open=Price.from_str(str(row["open"])),
                high=Price.from_str(str(row["high"])),
                low=Price.from_str(str(row["low"])),
                close=Price.from_str(str(row["close"])),
                volume=Quantity.from_str(f'{row["volume"]:.{size_precision}f}'),
                ts_event=ts,
                ts_init=ts,
            )
            bars.append(bar)
        return bars

    def _resolve_strategy_class(self, strategy: str):
        import importlib
        registry = {
            "pairs": "strategies.nt.pairs_trading_nt.PairsTradingNT",
            "hurst_vpin": "strategies.nt.hurst_vpin_nt.HurstVPINNT",
            "as_mm": "strategies.nt.as_mm_nt.ASMarketMakingNT",
            "obi": "strategies.nt.obi_nt.OBINT",
            "grid_mm": "strategies.nt.grid_mm_nt.GridMMNT",
            "composite_mm": "strategies.nt.composite_mm_nt.CompositeMMNT",
            "iceberg": "strategies.nt.iceberg_nt.IcebergNT",
        }
        path = registry.get(strategy)
        if not path:
            return None
        module_path, class_name = path.rsplit(".", 1)
        mod = importlib.import_module(module_path)
        return getattr(mod, class_name)

    def _extract_result(
        self,
        result,
        engine,
        strategy: str,
        interval: str,
        n_bars: int,
    ) -> dict:
        try:
            pnl = float(sum(
                a.pnl() for a in result.accounts if hasattr(a, 'pnl')
            )) if hasattr(result, 'accounts') else 0.0
        except Exception:
            pnl = 0.0

        try:
            equity = result.equity_curve if hasattr(result, 'equity_curve') else None
        except Exception:
            equity = None

        equity_vals = []
        if equity is not None and hasattr(equity, '__iter__'):
            equity_vals = [float(v) for v in equity] if equity is not None else []

        total_return = (equity_vals[-1] / 10000.0 - 1) * 100 if equity_vals else 0.0

        return {
            "strategy": strategy,
            "engine": "nautilus_trader",
            "interval": interval,
            "n_bars": n_bars,
            "start_equity": 10000.0,
            "end_equity": equity_vals[-1] if equity_vals else 10000.0,
            "total_return_pct": round(total_return, 2),
            "pnl": round(pnl, 2),
            "sharpe": self._compute_sharpe(equity_vals),
            "max_drawdown_pct": round(self._compute_max_dd(equity_vals) * 100, 2),
            "generated_at": datetime.now(timezone.utc).isoformat(),
        }

    def _compute_sharpe(self, equity: list[float]) -> float:
        if len(equity) < 2:
            return 0.0
        returns = [(equity[i] - equity[i - 1]) / equity[i - 1] for i in range(1, len(equity))]
        mean_ret = np.mean(returns) if returns else 0.0
        std_ret = np.std(returns, ddof=1) if returns else 0.0
        return (mean_ret / std_ret) * np.sqrt(365 * 24) if std_ret > 0 else 0.0

    def _compute_max_dd(self, equity: list[float]) -> float:
        if not equity:
            return 0.0
        peak = equity[0]
        worst = 0.0
        for v in equity:
            if v > peak:
                peak = v
            dd = (peak - v) / peak if peak > 0 else 0.0
            worst = max(worst, dd)
        return worst
