"""StrategyRunner — orchestrates strategies in live or backtest mode.

Live mode connects to real-time data via the broker adapter and routes
orders through the ``BrokerRouter``. Backtest mode uses the
``BacktestEngine`` to replay historical data through the same strategy code.
"""

from __future__ import annotations

import logging
import signal
import time
from typing import TYPE_CHECKING, Any, Dict, List, Optional

from ..events.bus import EventBus
from ..model.events import BarEvent, QuoteEvent
from .backtest import BacktestEngine, BacktestResult
from .base import Strategy
from .context import Cache, Clock, Portfolio

if TYPE_CHECKING:
    from ..clients.sync import CPZClient

logger = logging.getLogger(__name__)


class StrategyRunner:
    """Run one or more strategies in live or backtest mode.

    In live mode the runner polls the broker for data and dispatches
    events to strategy handlers. In backtest mode it delegates to
    :class:`BacktestEngine` for deterministic historical replay.

    Args:
        client: A ``CPZClient`` instance (provides data + execution).
        mode: ``"live"`` or ``"backtest"`` (default ``"backtest"``).
    """

    def __init__(self, client: "CPZClient", mode: str = "backtest") -> None:
        self._client = client
        self._mode = mode.lower()
        self._strategies: Dict[str, Strategy] = {}
        self._bus = EventBus()
        self._running = False

    # ------------------------------------------------------------------
    # Strategy management
    # ------------------------------------------------------------------

    def add_strategy(self, strategy: Strategy) -> None:
        """Register a strategy with the runner."""
        sid = strategy.strategy_id
        if sid in self._strategies:
            raise ValueError(f"Strategy {sid} is already registered")

        strategy._bus = self._bus
        strategy.cache = Cache()
        strategy.clock = Clock()

        if self._mode == "live":
            strategy._router = self._client.execution.router
            strategy.portfolio = Portfolio()

        self._strategies[sid] = strategy
        logger.info("Registered strategy %s", sid)

    def remove_strategy(self, strategy_id: str) -> None:
        """Remove a strategy by ID."""
        strat = self._strategies.pop(strategy_id, None)
        if strat and strat._running:
            strat.on_stop()
            strat._running = False

    def list_strategies(self) -> List[str]:
        return list(self._strategies.keys())

    # ------------------------------------------------------------------
    # Backtest entry point
    # ------------------------------------------------------------------

    def backtest(
        self,
        start: str,
        end: str,
        initial_cash: float = 100_000.0,
        timeframe: str = "1D",
        commission_pct: float = 0.0003,
        slippage_pct: float = 0.0001,
    ) -> BacktestResult:
        """Run a backtest over historical data.

        Args:
            start: Start date (YYYY-MM-DD).
            end: End date (YYYY-MM-DD).
            initial_cash: Starting cash (default $100k).
            timeframe: Bar timeframe for data fetching (default ``1D``).
            commission_pct: Commission as fraction of trade value (default 3 bps).
            slippage_pct: Slippage as fraction of price (default 1 bp).

        Returns:
            :class:`BacktestResult` with equity curve, trades, and metrics.
        """
        if not self._strategies:
            raise ValueError("No strategies registered. Call add_strategy() first.")

        engine = BacktestEngine(
            client=self._client,
            bus=self._bus,
            initial_cash=initial_cash,
            commission_pct=commission_pct,
            slippage_pct=slippage_pct,
        )

        strategies = list(self._strategies.values())
        return engine.run(
            strategies=strategies,
            start=start,
            end=end,
            timeframe=timeframe,
        )

    # ------------------------------------------------------------------
    # Live entry point
    # ------------------------------------------------------------------

    def run(self, poll_interval: float = 1.0) -> None:
        """Start live trading — blocks until interrupted.

        Polls the broker for data and dispatches events to strategies.
        Press Ctrl+C to stop gracefully.
        """
        if self._mode != "live":
            raise RuntimeError("Call backtest() for backtest mode, or set mode='live'")

        if not self._strategies:
            raise ValueError("No strategies registered. Call add_strategy() first.")

        self._running = True
        _original_handler = signal.getsignal(signal.SIGINT)

        def _stop(sig: Any, frame: Any) -> None:
            logger.info("Received interrupt — stopping strategies")
            self._running = False

        signal.signal(signal.SIGINT, _stop)

        try:
            for strat in self._strategies.values():
                strat._running = True
                strat.on_start()

            logger.info("Live runner started with %d strategies", len(self._strategies))

            while self._running:
                self._poll_live_data()
                time.sleep(poll_interval)

        except KeyboardInterrupt:
            pass
        finally:
            for strat in self._strategies.values():
                try:
                    strat.on_stop()
                except Exception as exc:
                    logger.warning("Error stopping %s: %s", strat.strategy_id, exc)
                strat._running = False
            self._running = False
            signal.signal(signal.SIGINT, _original_handler)
            logger.info("Live runner stopped")

    def _poll_live_data(self) -> None:
        """Fetch latest data from broker and publish to the event bus."""
        all_symbols: set[str] = set()
        for strat in self._strategies.values():
            for sub in strat._subscriptions:
                all_symbols.add(sub["symbol"])

        if not all_symbols:
            return

        try:
            quotes = self._client.execution.router.get_quotes(list(all_symbols))
            for q in quotes:
                evt = QuoteEvent(
                    symbol=q.symbol,
                    bid=q.bid,
                    ask=q.ask,
                    bid_size=getattr(q, "bid_size", 0),
                    ask_size=getattr(q, "ask_size", 0),
                )
                self._bus.publish(f"quote.{q.symbol}", evt)
        except Exception as exc:
            logger.debug("Quote poll failed: %s", exc)

        for strat in self._strategies.values():
            for sub in strat._subscriptions:
                if sub["type"] == "bar":
                    try:
                        bars = self._client.data.bars(
                            sub["symbol"],
                            timeframe=sub.get("timeframe", "1Min"),
                            limit=1,
                        )
                        if bars is not None and len(bars) > 0:
                            latest = bars.iloc[-1] if hasattr(bars, "iloc") else bars[-1]
                            evt = BarEvent(
                                symbol=sub["symbol"],
                                timeframe=sub.get("timeframe", "1Min"),
                                open=float(latest.get("open", latest.get("o", 0))),
                                high=float(latest.get("high", latest.get("h", 0))),
                                low=float(latest.get("low", latest.get("l", 0))),
                                close=float(latest.get("close", latest.get("c", 0))),
                                volume=float(latest.get("volume", latest.get("v", 0))),
                            )
                            self._bus.publish(
                                f"bar.{sub['symbol']}.{sub.get('timeframe', '1Min')}", evt
                            )
                    except Exception as exc:
                        logger.debug("Bar poll failed for %s: %s", sub["symbol"], exc)
