"""Strategy lifecycle controller: deploy, stop, pause, resume strategies as threads."""

from __future__ import annotations

import logging
import threading
import time
from typing import Any

from horizon._horizon import Engine, Market, RiskConfig
from horizon.fund._types import FundConfig, StrategyConfig, StrategyState, StrategyStatus

logger = logging.getLogger("horizon.fund.controller")


class _StrategyHandle:
    """Internal state for a running strategy."""

    __slots__ = (
        "config", "engine", "thread", "state", "start_time",
        "stop_event", "pause_event", "error", "allocated_capital",
        "hwm_pnl", "retry_count", "_last_fill_count",
    )

    def __init__(self, config: StrategyConfig, engine: Engine, allocated_capital: float) -> None:
        self.config = config
        self.engine = engine
        self.thread: threading.Thread | None = None
        self.state = StrategyState.PENDING
        self.start_time = time.time()
        self.stop_event = threading.Event()
        self.pause_event = threading.Event()  # Set = paused
        self.error: str | None = None
        self.allocated_capital = allocated_capital
        self.hwm_pnl: float = allocated_capital
        self.retry_count: int = 0
        self._last_fill_count: int = 0


class StrategyController:
    """Manages strategy lifecycle: deploy, stop, pause, resume.

    Each strategy runs in a daemon thread with its own Engine instance.
    The controller tracks state transitions and provides status.
    """

    def __init__(self, config: FundConfig) -> None:
        self._config = config
        self._strategies: dict[str, _StrategyHandle] = {}
        self._lock = threading.Lock()

    @property
    def strategy_names(self) -> list[str]:
        with self._lock:
            return list(self._strategies.keys())

    @property
    def engines(self) -> dict[str, Engine]:
        """Return mapping of strategy_name -> Engine for all tracked strategies."""
        with self._lock:
            return {
                name: h.engine
                for name, h in self._strategies.items()
                if h.engine is not None
            }

    def deploy(
        self,
        config: StrategyConfig,
        allocated_capital: float,
        risk_config: RiskConfig | None = None,
    ) -> str:
        """Deploy a new strategy in a background thread.

        Args:
            config: Strategy configuration.
            allocated_capital: Capital allocated to this strategy.
            risk_config: Risk config (scaled to allocation). If None, uses defaults.

        Returns:
            Strategy name.

        Raises:
            ValueError: If strategy name already exists or max strategies reached.
        """
        with self._lock:
            if config.name in self._strategies:
                raise ValueError(f"Strategy '{config.name}' already exists")
            if len(self._strategies) >= self._config.max_strategies:
                raise ValueError(
                    f"Max strategies ({self._config.max_strategies}) reached"
                )

        # Create engine for this strategy
        engine = self._create_strategy_engine(config, risk_config)
        handle = _StrategyHandle(config, engine, allocated_capital)

        with self._lock:
            self._strategies[config.name] = handle

        # Start strategy thread
        thread = threading.Thread(
            target=self._strategy_thread,
            args=(config.name,),
            name=f"strategy-{config.name}",
            daemon=True,
        )
        handle.thread = thread
        handle.state = StrategyState.RUNNING
        thread.start()

        logger.info(
            "Deployed strategy '%s' with %.2f capital",
            config.name, allocated_capital,
        )
        return config.name

    def stop(self, name: str, timeout: float = 10.0) -> None:
        """Gracefully stop a running strategy.

        Args:
            name: Strategy name.
            timeout: Max seconds to wait for thread to finish.
        """
        with self._lock:
            handle = self._strategies.get(name)
            if handle is None:
                raise ValueError(f"Strategy '{name}' not found")
            if handle.state in (StrategyState.STOPPED, StrategyState.RETIRED):
                return
            handle.state = StrategyState.STOPPING

        # Signal the thread to stop
        handle.stop_event.set()
        handle.pause_event.clear()  # Unpause if paused so it can exit

        if handle.thread is not None and handle.thread.is_alive():
            handle.thread.join(timeout=timeout)

        # Cancel all orders on the engine
        try:
            handle.engine.cancel_all()
        except Exception as e:
            logger.warning("Error canceling orders for '%s': %s", name, e)

        with self._lock:
            handle.state = StrategyState.STOPPED
        logger.info("Stopped strategy '%s'", name)

    def pause(self, name: str) -> None:
        """Pause a running strategy (stops quoting, keeps engine alive)."""
        with self._lock:
            handle = self._strategies.get(name)
            if handle is None:
                raise ValueError(f"Strategy '{name}' not found")
            if handle.state != StrategyState.RUNNING:
                raise ValueError(f"Strategy '{name}' is {handle.state.value}, not running")
            handle.state = StrategyState.PAUSED
            handle.pause_event.set()

        # Cancel open orders when pausing
        try:
            handle.engine.cancel_all()
        except Exception:
            pass

        logger.info("Paused strategy '%s'", name)

    def resume(self, name: str) -> None:
        """Resume a paused strategy."""
        with self._lock:
            handle = self._strategies.get(name)
            if handle is None:
                raise ValueError(f"Strategy '{name}' not found")
            if handle.state != StrategyState.PAUSED:
                raise ValueError(f"Strategy '{name}' is {handle.state.value}, not paused")
            handle.state = StrategyState.RUNNING
            handle.pause_event.clear()

        logger.info("Resumed strategy '%s'", name)

    def retire(self, name: str, timeout: float = 10.0) -> None:
        """Retire a strategy permanently. Stops it and removes from active tracking."""
        self.stop(name, timeout=timeout)
        with self._lock:
            handle = self._strategies.get(name)
            if handle is not None:
                handle.state = StrategyState.RETIRED
        logger.info("Retired strategy '%s'", name)

    def remove(self, name: str) -> None:
        """Remove a stopped/retired strategy from tracking entirely."""
        with self._lock:
            handle = self._strategies.get(name)
            if handle is None:
                return
            if handle.state not in (StrategyState.STOPPED, StrategyState.RETIRED, StrategyState.FAILED):
                raise ValueError(f"Cannot remove strategy '{name}' in state {handle.state.value}")
            del self._strategies[name]

    def status(self, name: str) -> StrategyStatus:
        """Get status of a single strategy."""
        with self._lock:
            handle = self._strategies.get(name)
            if handle is None:
                raise ValueError(f"Strategy '{name}' not found")
            return self._handle_to_status(handle)

    def all_statuses(self) -> list[StrategyStatus]:
        """Get status of all strategies."""
        with self._lock:
            return [self._handle_to_status(h) for h in self._strategies.values()]

    def running_count(self) -> int:
        with self._lock:
            return sum(
                1 for h in self._strategies.values()
                if h.state == StrategyState.RUNNING
            )

    def failed_strategies(self) -> list[str]:
        with self._lock:
            return [
                name for name, h in self._strategies.items()
                if h.state == StrategyState.FAILED
            ]

    def strategy_drawdowns(self) -> dict[str, float]:
        """Get drawdown percentage for each running strategy.

        Tracks per-strategy HWM and computes drawdown as:
        max((hwm - current_nav) / hwm * 100, 0.0)
        where current_nav = allocated_capital + realized + unrealized.
        """
        result: dict[str, float] = {}
        with self._lock:
            for name, h in self._strategies.items():
                if h.state != StrategyState.RUNNING:
                    continue
                try:
                    s = h.engine.status()
                    current_nav = h.allocated_capital + s.total_realized_pnl + s.total_unrealized_pnl
                    # Update HWM
                    if current_nav > h.hwm_pnl:
                        h.hwm_pnl = current_nav
                    # Compute drawdown from HWM
                    if h.hwm_pnl > 0:
                        result[name] = max((h.hwm_pnl - current_nav) / h.hwm_pnl * 100.0, 0.0)
                    else:
                        result[name] = 0.0
                except Exception:
                    result[name] = 0.0
        return result

    def restart(self, name: str, timeout: float = 10.0) -> str:
        """Restart a failed strategy using its saved config and capital.

        Stops, removes, and redeploys the strategy.

        Returns:
            Strategy name.
        """
        with self._lock:
            handle = self._strategies.get(name)
            if handle is None:
                raise ValueError(f"Strategy '{name}' not found")
            config = handle.config
            capital = handle.allocated_capital

        self.stop(name, timeout=timeout)
        self.remove(name)
        return self.deploy(config, capital)

    def stop_all(self, timeout: float = 10.0) -> None:
        """Stop all running strategies."""
        names = self.strategy_names
        for name in names:
            try:
                self.stop(name, timeout=timeout)
            except Exception as e:
                logger.warning("Error stopping '%s': %s", name, e)

    # ------------------------------------------------------------------
    # Internal
    # ------------------------------------------------------------------

    def _create_strategy_engine(
        self,
        config: StrategyConfig,
        risk_config: RiskConfig | None,
    ) -> Engine:
        """Create an Engine instance for a strategy.

        Uses the same _create_engine logic from strategy.py but
        imports it to avoid circular dependencies.
        """
        from horizon.strategy import _create_engine

        exchange_list = []
        if config.exchange is not None:
            if isinstance(config.exchange, list):
                exchange_list = config.exchange
            else:
                exchange_list = [config.exchange]

        return _create_engine(
            exchange_list=exchange_list,
            risk_config=risk_config,
            mode=config.mode,
        )

    def _strategy_thread(self, name: str) -> None:
        """Main loop for a strategy thread.

        Runs a simplified version of the strategy loop: builds context,
        runs pipeline, processes results, respects pause/stop signals.
        """
        with self._lock:
            handle = self._strategies.get(name)
            if handle is None:
                return

        config = handle.config
        engine = handle.engine
        interval = self._config.strategy_cycle_secs

        # Resolve markets
        markets = self._resolve_markets(config)

        # Start feeds
        if config.feeds:
            try:
                from horizon.strategy import _start_feeds
                _start_feeds(engine, config.feeds)
            except Exception as e:
                logger.error("Failed to start feeds for '%s': %s", name, e)

        # Set daily baseline
        try:
            status = engine.status()
            baseline = status.total_realized_pnl + status.total_unrealized_pnl
            engine.set_daily_baseline(baseline)
        except Exception:
            pass

        max_retries = self._config.auto_restart_max_retries

        cycle = 0
        while not handle.stop_event.is_set():
            try:
                while not handle.stop_event.is_set():
                    # Check pause
                    if handle.pause_event.is_set():
                        handle.stop_event.wait(timeout=0.5)
                        continue

                    cycle_start = time.monotonic()
                    cycle += 1

                    try:
                        self._run_strategy_cycle(handle, markets, cycle)
                    except Exception as e:
                        logger.error("Strategy '%s' cycle error: %s", name, e)

                    elapsed = time.monotonic() - cycle_start
                    remaining = max(0.0, interval - elapsed)
                    if remaining > 0:
                        handle.stop_event.wait(timeout=remaining)

                # Normal exit (stop_event set)
                break

            except Exception as e:
                handle.retry_count += 1
                if handle.retry_count <= max_retries and not handle.stop_event.is_set():
                    backoff = min(2 ** handle.retry_count, 30)
                    logger.warning(
                        "Strategy '%s' failed (attempt %d/%d), retrying in %ds: %s",
                        name, handle.retry_count, max_retries, backoff, e,
                    )
                    handle.stop_event.wait(timeout=backoff)
                    continue
                else:
                    logger.error("Strategy '%s' failed permanently: %s", name, e)
                    with self._lock:
                        handle.state = StrategyState.FAILED
                        handle.error = str(e)
                    return

        with self._lock:
            if handle.state == StrategyState.STOPPING:
                handle.state = StrategyState.STOPPED

    def _run_strategy_cycle(
        self,
        handle: _StrategyHandle,
        markets: list[Market],
        cycle: int,
    ) -> None:
        """Run one cycle of a strategy's pipeline."""
        from horizon.strategy import _build_context, _run_pipeline, _process_result

        engine = handle.engine
        config = handle.config
        is_live = any(n != "paper" for n in engine.exchange_names())

        if is_live:
            engine.poll_fills()

        # Update daily P&L
        status = engine.status()
        current_pnl = status.total_realized_pnl + status.total_unrealized_pnl
        engine.update_daily_pnl(current_pnl)

        # Inject adaptive execution parameters
        params = dict(config.params)
        if hasattr(self, '_adaptive_tuner') and self._adaptive_tuner is not None:
            adaptive_params = self._adaptive_tuner.current_params(handle.config.name)
            if adaptive_params:
                params.update(adaptive_params)

        for market in markets:
            try:
                ctx = _build_context(
                    engine, market, config.feeds, params,
                    cached_status=status,
                )

                # Resolve pipeline
                pipeline = config.pipeline
                if isinstance(pipeline, dict):
                    pipeline = pipeline.get(market.id, pipeline.get("*"))
                    if pipeline is None:
                        continue

                result = _run_pipeline(pipeline, ctx)
                if result is not None:
                    _process_result(engine, market, result, ctx)

            except Exception as e:
                logger.debug(
                    "Pipeline error for %s/%s: %s",
                    handle.config.name, market.id, e,
                )

        # Record fills to exec quality tracker
        if hasattr(self, '_exec_quality') and self._exec_quality is not None:
            try:
                recent = engine.recent_fills()
                new_count = len(recent)
                if new_count > handle._last_fill_count:
                    new_fills = recent[handle._last_fill_count:]
                    for fill in new_fills:
                        # Get mid price from feed snapshot if available
                        mid_price = fill.price
                        try:
                            snap = engine.feed_snapshot(fill.market_id)
                            if snap is not None and snap.price > 0:
                                mid_price = snap.price
                        except Exception:
                            pass
                        side_str = "buy" if str(fill.order_side) == "OrderSide.Buy" else "sell"
                        self._exec_quality.record_fill(
                            strategy=handle.config.name,
                            expected_price=fill.price,
                            fill_price=fill.price,
                            mid_price_at_fill=mid_price,
                            size=fill.size,
                            side=side_str,
                        )
                    handle._last_fill_count = new_count
            except Exception as e:
                logger.debug("Fill recording error for '%s': %s", handle.config.name, e)

        # Periodic maintenance
        if cycle % 10 == 0:
            engine.check_lifecycle()
        if cycle % 100 == 0:
            engine.evict_stale_orders(300.0)

    @staticmethod
    def _resolve_markets(config: StrategyConfig) -> list[Market]:
        """Resolve market slugs/IDs to Market objects."""
        markets = []
        for slug in config.markets:
            markets.append(Market(id=slug, name=slug))
        return markets

    def _handle_to_status(self, handle: _StrategyHandle) -> StrategyStatus:
        """Convert a _StrategyHandle to a StrategyStatus.

        NOTE: This method intentionally mutates ``handle.hwm_pnl`` as a side
        effect.  The high-water mark must be updated every time NAV is computed
        so that drawdown tracking stays accurate.  The caller always holds
        ``self._lock``, so the mutation is thread-safe.
        """
        try:
            engine_status = handle.engine.status()
            pnl = engine_status.total_realized_pnl + engine_status.total_unrealized_pnl
            # Intentional side-effect: keep HWM current on every status read
            # so drawdown is always accurate (see docstring above).
            current_nav = handle.allocated_capital + pnl
            if current_nav > handle.hwm_pnl:
                handle.hwm_pnl = current_nav
            dd = max((handle.hwm_pnl - current_nav) / handle.hwm_pnl * 100.0, 0.0) if handle.hwm_pnl > 0 else 0.0
            open_orders = engine_status.open_orders
            active_positions = engine_status.active_positions
        except Exception:
            pnl = 0.0
            dd = 0.0
            open_orders = 0
            active_positions = 0

        return StrategyStatus(
            name=handle.config.name,
            state=handle.state,
            allocated_capital=handle.allocated_capital,
            current_nav=handle.allocated_capital + pnl,
            pnl=pnl,
            drawdown_pct=dd,
            open_orders=open_orders,
            active_positions=active_positions,
            uptime_secs=time.time() - handle.start_time,
            error=handle.error,
        )
