"""BacktestEngine — deterministic historical replay with fill simulation.

Replays bars through strategy handlers in timestamp order, simulates
fills with configurable slippage and commission models, and produces
a ``BacktestResult`` with equity curve, trade log, and full analytics.
"""

from __future__ import annotations

import logging
import uuid
from dataclasses import dataclass, field
from datetime import datetime
from typing import TYPE_CHECKING, Any, Dict, List, Optional

import numpy as np

from ..events.bus import EventBus
from ..execution.enums import OrderSide, OrderType
from ..execution.models import Order, OrderSubmitRequest
from ..model.events import BarEvent, FillEvent, OrderEvent
from .base import Strategy
from .context import Cache, Clock, Portfolio

if TYPE_CHECKING:
    from ..clients.sync import CPZClient

logger = logging.getLogger(__name__)


@dataclass
class TradeRecord:
    """Single round-trip or open trade captured during backtest."""

    symbol: str
    side: str
    qty: float
    entry_price: float
    exit_price: float = 0.0
    entry_time: str = ""
    exit_time: str = ""
    pnl: float = 0.0
    commission: float = 0.0
    strategy_id: str = ""


@dataclass
class BacktestResult:
    """Complete results from a backtest run."""

    equity_curve: List[float] = field(default_factory=list)
    timestamps: List[str] = field(default_factory=list)
    trades: List[TradeRecord] = field(default_factory=list)
    initial_cash: float = 100_000.0
    final_equity: float = 0.0
    total_return_pct: float = 0.0
    sharpe_ratio: float = 0.0
    sortino_ratio: float = 0.0
    max_drawdown_pct: float = 0.0
    win_rate: float = 0.0
    profit_factor: float = 0.0
    total_trades: int = 0
    winning_trades: int = 0
    losing_trades: int = 0
    avg_win: float = 0.0
    avg_loss: float = 0.0
    total_commission: float = 0.0
    calmar_ratio: float = 0.0
    avg_trade_pnl: float = 0.0
    max_consecutive_wins: int = 0
    max_consecutive_losses: int = 0
    start_date: str = ""
    end_date: str = ""
    timeframe: str = ""

    def summary(self) -> str:
        """Human-readable summary of backtest results."""
        lines = [
            "=" * 60,
            "  BACKTEST RESULTS",
            "=" * 60,
            f"  Period:            {self.start_date} → {self.end_date}",
            f"  Timeframe:         {self.timeframe}",
            f"  Initial Cash:      ${self.initial_cash:,.2f}",
            f"  Final Equity:      ${self.final_equity:,.2f}",
            f"  Total Return:      {self.total_return_pct:+.2f}%",
            "-" * 60,
            f"  Sharpe Ratio:      {self.sharpe_ratio:.3f}",
            f"  Sortino Ratio:     {self.sortino_ratio:.3f}",
            f"  Calmar Ratio:      {self.calmar_ratio:.3f}",
            f"  Max Drawdown:      {self.max_drawdown_pct:.2f}%",
            "-" * 60,
            f"  Total Trades:      {self.total_trades}",
            f"  Win Rate:          {self.win_rate:.1f}%",
            f"  Profit Factor:     {self.profit_factor:.2f}",
            f"  Avg Win:           ${self.avg_win:,.2f}",
            f"  Avg Loss:          ${self.avg_loss:,.2f}",
            f"  Avg Trade P&L:     ${self.avg_trade_pnl:,.2f}",
            f"  Commission:        ${self.total_commission:,.2f}",
            f"  Max Consec Wins:   {self.max_consecutive_wins}",
            f"  Max Consec Losses: {self.max_consecutive_losses}",
            "=" * 60,
        ]
        return "\n".join(lines)


class _SimulatedRouter:
    """Minimal router substitute for the backtest engine.

    Simulates order fills at the next bar's open with slippage.
    """

    def __init__(
        self,
        portfolio: Portfolio,
        bus: EventBus,
        slippage_pct: float = 0.0001,
        commission_pct: float = 0.0003,
    ) -> None:
        self._portfolio = portfolio
        self._bus = bus
        self._slippage_pct = slippage_pct
        self._commission_pct = commission_pct
        self._pending: List[OrderSubmitRequest] = []
        self._filled_orders: List[TradeRecord] = []
        self._current_bar: Optional[BarEvent] = None

    def submit_order(self, req: OrderSubmitRequest) -> Order:
        oid = uuid.uuid4().hex[:12]
        self._pending.append(req)
        return Order(
            id=oid,
            symbol=req.symbol,
            side=req.side,
            qty=req.qty or 0.0,
            type=req.type,
            time_in_force=req.time_in_force,
            status="pending",
        )

    def cancel_order(self, order_id: str) -> Order:
        return Order(
            id=order_id, symbol="", side=OrderSide.BUY, qty=0,
            type=OrderType.MARKET, time_in_force="DAY", status="canceled",
        )

    def list_orders(self, status: str | None = None, **kw: Any) -> list:
        return []

    def process_pending(self, bar: BarEvent, strategies: List[Strategy]) -> None:
        """Fill pending orders at the bar's open price with slippage."""
        self._current_bar = bar
        pending = [r for r in self._pending if r.symbol == bar.symbol]
        self._pending = [r for r in self._pending if r.symbol != bar.symbol]

        for req in pending:
            fill_price = bar.open
            if req.side == OrderSide.BUY:
                fill_price *= 1 + self._slippage_pct
            else:
                fill_price *= 1 - self._slippage_pct

            qty = req.qty or (req.notional / fill_price if req.notional else 0)
            if qty <= 0:
                continue

            commission = qty * fill_price * self._commission_pct

            fill = FillEvent(
                order_id=uuid.uuid4().hex[:12],
                symbol=req.symbol,
                side=req.side.value,
                fill_qty=qty,
                fill_price=fill_price,
                commission=commission,
                strategy_id=req.strategy_id,
            )

            for strat in strategies:
                if strat.strategy_id == req.strategy_id:
                    strat._handle_fill(fill)
                    break

            ts = datetime.utcfromtimestamp(bar.ts_event / 1e9).isoformat() if bar.ts_event else ""
            self._filled_orders.append(
                TradeRecord(
                    symbol=req.symbol,
                    side=req.side.value,
                    qty=qty,
                    entry_price=fill_price,
                    entry_time=ts,
                    commission=commission,
                    strategy_id=req.strategy_id,
                )
            )


class BacktestEngine:
    """Historical replay engine with deterministic fill simulation."""

    def __init__(
        self,
        client: "CPZClient",
        bus: EventBus,
        initial_cash: float = 100_000.0,
        commission_pct: float = 0.0003,
        slippage_pct: float = 0.0001,
    ) -> None:
        self._client = client
        self._bus = bus
        self._initial_cash = initial_cash
        self._commission_pct = commission_pct
        self._slippage_pct = slippage_pct

    def run(
        self,
        strategies: List[Strategy],
        start: str,
        end: str,
        timeframe: str = "1D",
    ) -> BacktestResult:
        portfolio = Portfolio(self._initial_cash)
        sim_router = _SimulatedRouter(
            portfolio=portfolio,
            bus=self._bus,
            slippage_pct=self._slippage_pct,
            commission_pct=self._commission_pct,
        )
        clock = Clock()

        all_symbols: set[str] = set()
        for strat in strategies:
            strat.portfolio = portfolio
            strat.clock = clock
            strat.cache = Cache()
            strat._router = sim_router  # type: ignore[assignment]
            strat._bus = self._bus
            for inst in strat.config.instruments:
                all_symbols.add(inst)

        for strat in strategies:
            strat._running = True
            strat.on_start()
            for sub in strat._subscriptions:
                all_symbols.add(sub["symbol"])

        if not all_symbols:
            raise ValueError("No instruments to backtest — set config.instruments or subscribe_bars()")

        symbol_bars = self._fetch_data(list(all_symbols), start, end, timeframe)

        merged = []
        for symbol, bars_list in symbol_bars.items():
            for bar in bars_list:
                merged.append((bar, symbol))

        merged.sort(key=lambda x: x[0].ts_event)

        equity_curve: List[float] = [self._initial_cash]
        timestamps: List[str] = [start]

        for bar, symbol in merged:
            clock.set_time(bar.ts_event)
            sim_router.process_pending(bar, strategies)
            portfolio.update_price(symbol, bar.close)

            topic = f"bar.{symbol}.{timeframe}"
            self._bus.publish(topic, bar)

            for strat in strategies:
                has_sub = any(
                    s["symbol"] == symbol and s.get("timeframe", "1D") == timeframe
                    for s in strat._subscriptions
                )
                if has_sub:
                    strat._handle_bar(bar)

            equity_curve.append(portfolio.equity())
            ts_str = datetime.utcfromtimestamp(bar.ts_event / 1e9).isoformat() if bar.ts_event else ""
            timestamps.append(ts_str)

        for strat in strategies:
            strat.on_stop()
            strat._running = False

        return self._build_result(
            equity_curve=equity_curve,
            timestamps=timestamps,
            trades=sim_router._filled_orders,
            portfolio=portfolio,
            start=start,
            end=end,
            timeframe=timeframe,
        )

    def _fetch_data(
        self, symbols: List[str], start: str, end: str, timeframe: str
    ) -> Dict[str, List[BarEvent]]:
        """Fetch historical bars via the DataClient and convert to BarEvents."""
        result: Dict[str, List[BarEvent]] = {}

        for symbol in symbols:
            try:
                df = self._client.data.bars(
                    symbol, timeframe=timeframe, start=start, end=end, limit=10000
                )
            except Exception as exc:
                logger.warning("Failed to fetch data for %s: %s", symbol, exc)
                continue

            bars: List[BarEvent] = []
            if df is None:
                continue

            try:
                import pandas as pd

                if isinstance(df, pd.DataFrame) and not df.empty:
                    for idx, row in df.iterrows():
                        ts = idx if isinstance(idx, pd.Timestamp) else pd.Timestamp(str(idx))
                        ts_ns = int(ts.timestamp() * 1e9)
                        bars.append(
                            BarEvent(
                                symbol=symbol,
                                timeframe=timeframe,
                                open=float(row.get("open", row.get("o", 0))),
                                high=float(row.get("high", row.get("h", 0))),
                                low=float(row.get("low", row.get("l", 0))),
                                close=float(row.get("close", row.get("c", 0))),
                                volume=float(row.get("volume", row.get("v", 0))),
                                vwap=float(row.get("vwap", row.get("vw", 0))),
                                ts_event=ts_ns,
                                ts_init=ts_ns,
                            )
                        )
            except ImportError:
                pass

            if bars:
                result[symbol] = bars
                logger.info("Loaded %d bars for %s", len(bars), symbol)

        return result

    def _build_result(
        self,
        equity_curve: List[float],
        timestamps: List[str],
        trades: List[TradeRecord],
        portfolio: Portfolio,
        start: str,
        end: str,
        timeframe: str,
    ) -> BacktestResult:
        final_eq = equity_curve[-1] if equity_curve else self._initial_cash
        total_ret = ((final_eq / self._initial_cash) - 1) * 100

        daily_returns: List[float] = []
        for i in range(1, len(equity_curve)):
            if equity_curve[i - 1] != 0:
                daily_returns.append(equity_curve[i] / equity_curve[i - 1] - 1)

        sharpe = sortino = max_dd = calmar = 0.0
        if daily_returns:
            arr = np.array(daily_returns)
            mean_r = float(np.mean(arr))
            std_r = float(np.std(arr, ddof=1)) if len(arr) > 1 else 0.0
            sharpe = (mean_r / std_r * np.sqrt(252)) if std_r > 0 else 0.0

            downside = arr[arr < 0]
            down_std = float(np.std(downside, ddof=1)) if len(downside) > 1 else 0.0
            sortino = (mean_r / down_std * np.sqrt(252)) if down_std > 0 else 0.0

            cumulative = np.cumprod(1 + arr)
            running_max = np.maximum.accumulate(cumulative)
            drawdowns = (cumulative - running_max) / running_max
            max_dd = float(np.min(drawdowns)) * 100
            calmar = (total_ret / abs(max_dd)) if max_dd != 0 else 0.0

        closed_pnls = [t.pnl for t in trades if t.exit_price > 0]
        total_t = len(trades)
        wins = [p for p in closed_pnls if p > 0]
        losses = [p for p in closed_pnls if p <= 0]
        win_rate = (len(wins) / len(closed_pnls) * 100) if closed_pnls else 0.0
        avg_win = float(np.mean(wins)) if wins else 0.0
        avg_loss = float(np.mean(losses)) if losses else 0.0
        gross_profit = sum(wins) if wins else 0.0
        gross_loss = abs(sum(losses)) if losses else 0.0
        profit_factor = (gross_profit / gross_loss) if gross_loss > 0 else float("inf")
        avg_pnl = float(np.mean(closed_pnls)) if closed_pnls else 0.0

        max_cw = max_cl = cw = cl = 0
        for p in closed_pnls:
            if p > 0:
                cw += 1
                cl = 0
            else:
                cl += 1
                cw = 0
            max_cw = max(max_cw, cw)
            max_cl = max(max_cl, cl)

        return BacktestResult(
            equity_curve=equity_curve,
            timestamps=timestamps,
            trades=trades,
            initial_cash=self._initial_cash,
            final_equity=final_eq,
            total_return_pct=total_ret,
            sharpe_ratio=sharpe,
            sortino_ratio=sortino,
            max_drawdown_pct=max_dd,
            win_rate=win_rate,
            profit_factor=profit_factor,
            total_trades=total_t,
            winning_trades=len(wins),
            losing_trades=len(losses),
            avg_win=avg_win,
            avg_loss=avg_loss,
            total_commission=portfolio.commission_total,
            calmar_ratio=calmar,
            avg_trade_pnl=avg_pnl,
            max_consecutive_wins=max_cw,
            max_consecutive_losses=max_cl,
            start_date=start,
            end_date=end,
            timeframe=timeframe,
        )
