"""Isolated backtest sandbox for StrategyPlugin instances.

The sandbox runs a strategy against historical bar data in strict walk-forward
mode (no look-ahead bias).  Every new or modified strategy MUST pass sandbox
backtesting before it can advance from ``trust_level="sandbox"`` to ``paper``.

Usage::

    from strategy_sdk.sandbox import StrategySandbox, SandboxParams

    sandbox = StrategySandbox(my_plugin, bars, SandboxParams())
    report = sandbox.run()
    print(report["total_pnl"], report["win_rate"])
"""

from __future__ import annotations

import time as _time
import uuid
from dataclasses import dataclass, field
from typing import Any

from strategy_sdk.types import (
    MarketSnapshot,
    PortfolioStateView,
    StrategyInitContext,
    StrategyIntent,
    StrategyRiskOpinion,
)


@dataclass
class SandboxParams:
    """Configurable parameters for the sandbox backtest."""

    contract_size: float = 100.0
    lots: float = 0.10
    initial_capital: float = 10000.0
    min_bars: int = 30
    min_trades: int = 10
    warmup_bars: int = 50
    symbol: str = "XAUUSD"
    timeframe: str = "M5"

    # loss limits — safety gates
    max_drawdown_pct: float = 25.0
    max_consecutive_losses: int = 5
    stop_trading_on: bool = True  # halt backtest when loss guards trip


@dataclass
class _TradeRecord:
    side: str
    entry_price: float
    entry_bar: int
    sl: float | None = None
    tp: float | None = None
    regime: str = ""
    strategy_id: str = ""
    confidence: float = 0.0

    outcome: str = ""
    exit_price: float = 0.0
    exit_bar: int = 0
    pnl: float = 0.0


class StrategySandbox:
    """Isolated backtest runner for a single StrategyPlugin.

    Walk-forward simulation:
      - Iterates bars sequentially.
      - At each bar, builds a MarketSnapshot with only bars *up to and
        including* the current index (no future data).
      - Calls ``plugin.on_market_snapshot(snapshot)``.
      - If the plugin returns an OPEN intent and no position is open, opens
        a simulated trade at the *next* bar's open.
      - Stop-loss / take-profit checked on each subsequent bar's high/low.
      - All P&L tracked independently of live trading state.

    Outputs a report dict that can be compared against sandbox gate criteria.
    """

    def __init__(
        self,
        plugin,
        bars: list[dict[str, Any]],
        params: SandboxParams | None = None,
    ):
        self._plugin = plugin
        self._bars = bars
        self._params = params or SandboxParams()
        self._account_id = f"sandbox-{uuid.uuid4().hex[:8]}"
        self._instance_id = f"{plugin.manifest().strategy_id}-sandbox"

    # -- main entry point -----------------------------------------------

    def run(self) -> dict[str, Any]:
        """Execute the backtest and return a report dict.

        Report keys:
          total_trades, wins, losses, total_pnl, win_rate, max_drawdown_pct,
          sharpe_ratio, avg_win, avg_loss, profit_factor, trades, guards_tripped
        """
        self._initialize_plugin()
        trades: list[_TradeRecord] = []
        open_trade: _TradeRecord | None = None
        guard_events: list[dict[str, Any]] = []
        equity_curve: list[float] = [self._params.initial_capital]

        min_idx = max(self._params.warmup_bars, 1)
        for i in range(min_idx, len(self._bars) - 1):
            # Check open trade exit on this bar
            if open_trade is not None:
                result = self._check_exit(open_trade, i)
                if result:
                    open_trade = result["trade"]
                    trades.append(open_trade)
                    equity_curve.append(equity_curve[-1] + open_trade.pnl)
                    open_trade = None

                    if self._params.stop_trading_on:
                        if self._trip_guards(trades, equity_curve):
                            guard_events.append(
                                {
                                    "bar": i,
                                    "reason": "loss guard tripped",
                                    "consecutive_losses": self._count_consecutive_losses(trades),
                                    "drawdown_pct": self._calc_drawdown_pct(equity_curve),
                                }
                            )
                            break

            # Look for new signal (only if flat)
            if open_trade is None:
                snapshot = self._build_snapshot(i)
                intent = self._plugin.on_market_snapshot(snapshot)
                if intent.action in ("OPEN", "BUY", "SELL"):
                    open_trade = self._open_trade(intent, i)

        # Force-close any remaining open trade at last bar
        if open_trade is not None:
            last_bar = self._bars[-1]
            close_price = float(last_bar.get("close", 0))
            pnl = self._calc_pnl(
                open_trade.side, open_trade.entry_price, close_price
            )
            open_trade.outcome = "WIN" if pnl > 0 else "LOSS"
            open_trade.exit_price = close_price
            open_trade.exit_bar = len(self._bars) - 1
            open_trade.pnl = pnl
            trades.append(open_trade)
            equity_curve.append(equity_curve[-1] + pnl)

        return self._build_report(trades, equity_curve, guard_events)

    # -- plugin init ----------------------------------------------------

    def _initialize_plugin(self) -> None:
        manifest = self._plugin.manifest()
        ctx = StrategyInitContext(
            strategy_id=manifest.strategy_id,
            strategy_version=manifest.version,
            config_revision="sandbox",
            symbol=self._params.symbol,
            timeframe=self._params.timeframe,
            mode="sandbox",
        )
        self._plugin.initialize(ctx)

    # -- snapshot builder (walk-forward, no look-ahead) -----------------

    def _build_snapshot(self, idx: int) -> MarketSnapshot:
        visible = self._bars[: idx + 1]
        current = self._bars[idx]
        return MarketSnapshot(
            symbol=self._params.symbol,
            timeframe=self._params.timeframe,
            timestamp_ms=int(current.get("time", _time.time() * 1000)),
            bars=visible,
            mid=float(current.get("close", 0)),
            indicators={"bar_index": idx},
            session_mode="NORMAL",
        )

    # -- trade opening (next-bar open) ----------------------------------

    def _open_trade(self, intent: StrategyIntent, signal_bar: int) -> _TradeRecord | None:
        next_idx = signal_bar + 1
        if next_idx >= len(self._bars):
            return None
        next_bar = self._bars[next_idx]
        entry = float(next_bar.get("open", next_bar.get("close", 0)))

        ep = intent.entry_plan
        xp = intent.exit_plan
        sl = ep.get("sl") or (xp.get("sl") if xp else None)
        tp = ep.get("tp") or (xp.get("tp") if xp else None)

        return _TradeRecord(
            side=intent.side,
            entry_price=entry,
            entry_bar=next_idx,
            sl=float(sl) if sl is not None else None,
            tp=float(tp) if tp is not None else None,
            strategy_id=intent.strategy_id,
            confidence=intent.confidence,
        )

    # -- exit checking (OCO: SL before TP) ------------------------------

    def _check_exit(self, trade: _TradeRecord, bar_idx: int) -> dict[str, Any] | None:
        next_bar = self._bars[bar_idx]
        high = float(next_bar.get("high", 0))
        low = float(next_bar.get("low", 0))

        entry = trade.entry_price
        sl = trade.sl
        tp = trade.tp

        if sl is None and tp is None:
            return None
        if sl is not None and sl <= 0:
            return None
        if tp is not None and tp <= 0:
            return None

        if trade.side == "BUY":
            if sl is not None and low <= sl:
                trade.outcome = "LOSS"
                trade.exit_price = sl
                trade.pnl = self._calc_pnl("BUY", entry, sl)
            elif tp is not None and high >= tp:
                trade.outcome = "WIN"
                trade.exit_price = tp
                trade.pnl = self._calc_pnl("BUY", entry, tp)
            else:
                return None
        else:
            if sl is not None and high >= sl:
                trade.outcome = "LOSS"
                trade.exit_price = sl
                trade.pnl = self._calc_pnl("SELL", entry, sl)
            elif tp is not None and low <= tp:
                trade.outcome = "WIN"
                trade.exit_price = tp
                trade.pnl = self._calc_pnl("SELL", entry, tp)
            else:
                return None

        trade.exit_bar = bar_idx
        return {"trade": trade}

    def _calc_pnl(self, side: str, entry: float, exit_p: float) -> float:
        size = self._params.contract_size * self._params.lots
        if side == "BUY":
            return round((exit_p - entry) * size, 2)
        return round((entry - exit_p) * size, 2)

    # -- safety guards --------------------------------------------------

    def _trip_guards(
        self, trades: list[_TradeRecord], equity_curve: list[float]
    ) -> bool:
        if self._count_consecutive_losses(trades) >= self._params.max_consecutive_losses:
            return True
        if self._calc_drawdown_pct(equity_curve) > self._params.max_drawdown_pct:
            return True
        return False

    @staticmethod
    def _count_consecutive_losses(trades: list[_TradeRecord]) -> int:
        count = 0
        for t in reversed(trades):
            if t.outcome == "LOSS":
                count += 1
            else:
                break
        return count

    @staticmethod
    def _calc_drawdown_pct(equity_curve: list[float]) -> float:
        if not equity_curve:
            return 0.0
        peak = max(equity_curve)
        if peak <= 0:
            return 0.0
        trough = min(equity_curve[equity_curve.index(peak) :])
        return round((peak - trough) / peak * 100, 2)

    # -- report builder -------------------------------------------------

    def _build_report(
        self,
        trades: list[_TradeRecord],
        equity_curve: list[float],
        guard_events: list[dict[str, Any]],
    ) -> dict[str, Any]:
        total = len(trades)
        wins = [t for t in trades if t.outcome == "WIN"]
        losses = [t for t in trades if t.outcome == "LOSS"]
        n_wins = len(wins)
        n_losses = len(losses)
        total_pnl = round(sum(t.pnl for t in trades), 2)
        win_rate = round(n_wins / total * 100, 2) if total > 0 else 0.0
        max_dd = self._calc_drawdown_pct(equity_curve)
        sharpe = self._calc_sharpe(equity_curve)

        avg_win = round(sum(t.pnl for t in wins) / n_wins, 2) if n_wins > 0 else 0.0
        avg_loss = round(sum(t.pnl for t in losses) / n_losses, 2) if n_losses > 0 else 0.0
        gross_profit = sum(t.pnl for t in wins)
        gross_loss = abs(sum(t.pnl for t in losses))
        profit_factor = round(gross_profit / gross_loss, 2) if gross_loss > 0 else 0.0

        score = 0.0
        if total >= self._params.min_trades:
            import math

            score = total_pnl * (win_rate / 100.0) * math.log1p(total)

        return {
            "total_trades": total,
            "wins": n_wins,
            "losses": n_losses,
            "total_pnl": total_pnl,
            "win_rate": win_rate,
            "max_drawdown_pct": max_dd,
            "sharpe_ratio": sharpe,
            "avg_win": avg_win,
            "avg_loss": avg_loss,
            "profit_factor": profit_factor,
            "score": round(score, 2),
            "guards_tripped": len(guard_events) > 0,
            "guard_events": guard_events,
            "equity_curve": equity_curve,
            "trades": [
                {
                    "side": t.side,
                    "entry": t.entry_price,
                    "exit": t.exit_price,
                    "sl": t.sl,
                    "tp": t.tp,
                    "pnl": t.pnl,
                    "outcome": t.outcome,
                    "entry_bar": t.entry_bar,
                    "exit_bar": t.exit_bar,
                }
                for t in trades
            ],
        }

    def _calc_sharpe(self, equity_curve: list[float]) -> float:
        if len(equity_curve) < 2:
            return 0.0
        returns = []
        for i in range(1, len(equity_curve)):
            prev = equity_curve[i - 1]
            if prev > 0:
                returns.append((equity_curve[i] - prev) / prev)
        if not returns:
            return 0.0
        import math

        mean = sum(returns) / len(returns)
        if mean == 0:
            return 0.0
        variance = sum((r - mean) ** 2 for r in returns) / len(returns)
        std = math.sqrt(variance)
        if std == 0:
            return 0.0
        # Annualised: scale based on timeframe (bars per year)
        bars_per_day = {"M1": 1440, "M5": 288, "M15": 96, "M30": 48, "H1": 24, "H4": 6, "D1": 1}
        bpd = bars_per_day.get(self._params.timeframe, 288)
        return round(mean / std * math.sqrt(bpd * 252), 2)
