"""Fund-level stress testing: scenarios applied across all strategies simultaneously."""

from __future__ import annotations

import logging
import time
from dataclasses import dataclass, field
from typing import Any

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


@dataclass(frozen=True)
class FundScenario:
    """A stress scenario applied at fund level."""
    name: str
    description: str = ""
    price_shock_pct: float = 0.0  # All positions shocked by this %
    correlation_override: float | None = None
    exchange_down: str | None = None  # Simulate exchange outage
    liquidity_reduction_pct: float = 0.0  # Orderbook depth reduction


@dataclass(frozen=True)
class FundScenarioResult:
    """Result of a single fund stress scenario."""
    scenario: FundScenario
    pnl_impact: float
    nav_after: float
    drawdown_pct: float
    strategies_breached: list[str]  # Strategies exceeding their drawdown limits
    worst_strategy: str
    worst_strategy_pnl: float
    details: dict[str, float] = field(default_factory=dict)


@dataclass(frozen=True)
class FundStressResult:
    """Aggregated results of all fund stress scenarios."""
    timestamp: float
    scenarios: list[FundScenarioResult]
    worst_scenario: str
    worst_pnl: float
    current_nav: float
    passes_all: bool

    def summary(self) -> dict[str, Any]:
        return {
            "worst_scenario": self.worst_scenario,
            "worst_pnl": self.worst_pnl,
            "current_nav": self.current_nav,
            "passes_all": self.passes_all,
            "scenarios": [
                {
                    "name": s.scenario.name,
                    "pnl_impact": s.pnl_impact,
                    "nav_after": s.nav_after,
                    "drawdown_pct": s.drawdown_pct,
                    "strategies_breached": s.strategies_breached,
                }
                for s in self.scenarios
            ],
        }


# Built-in fund scenarios
FUND_ALL_DOWN_10 = FundScenario(
    name="all_positions_down_10pct",
    description="All positions lose 10% of value",
    price_shock_pct=-10.0,
)
FUND_ALL_DOWN_20 = FundScenario(
    name="all_positions_down_20pct",
    description="All positions lose 20% of value",
    price_shock_pct=-20.0,
)
FUND_CORRELATION_SPIKE = FundScenario(
    name="correlation_spike",
    description="All strategies become 95% correlated (crisis)",
    correlation_override=0.95,
    price_shock_pct=-5.0,
)
FUND_LIQUIDITY_CRISIS = FundScenario(
    name="liquidity_crisis",
    description="80% reduction in orderbook depth",
    liquidity_reduction_pct=80.0,
    price_shock_pct=-3.0,
)
FUND_EXCHANGE_OUTAGE = FundScenario(
    name="exchange_outage",
    description="Primary exchange goes offline",
    exchange_down="polymarket",
    price_shock_pct=-5.0,
)
FUND_BLACK_SWAN = FundScenario(
    name="black_swan",
    description="Extreme tail event: 30% loss with correlation spike",
    price_shock_pct=-30.0,
    correlation_override=0.99,
)


DEFAULT_FUND_SCENARIOS = [
    FUND_ALL_DOWN_10,
    FUND_ALL_DOWN_20,
    FUND_CORRELATION_SPIKE,
    FUND_LIQUIDITY_CRISIS,
    FUND_EXCHANGE_OUTAGE,
    FUND_BLACK_SWAN,
]


class FundStressTester:
    """Runs stress scenarios across the entire fund portfolio.

    Unlike per-strategy stress tests (horizon.stress), this applies
    shocks to all strategies simultaneously and checks fund-level limits.
    """

    def __init__(
        self,
        max_fund_drawdown_pct: float = 15.0,
        max_strategy_drawdown_pct: float = 25.0,
    ) -> None:
        self._max_fund_dd = max_fund_drawdown_pct
        self._max_strat_dd = max_strategy_drawdown_pct

    def run(
        self,
        engines: dict[str, Any],
        strategy_capitals: dict[str, float],
        current_nav: float,
        hwm: float,
        scenarios: list[FundScenario] | None = None,
    ) -> FundStressResult:
        """Run all stress scenarios against current fund state.

        Args:
            engines: Mapping of strategy_name -> Engine.
            strategy_capitals: Mapping of strategy_name -> allocated capital.
            current_nav: Current fund NAV.
            hwm: High water mark.
            scenarios: Scenarios to test (defaults to all built-in).

        Returns:
            FundStressResult with per-scenario outcomes.
        """
        if scenarios is None:
            scenarios = DEFAULT_FUND_SCENARIOS

        results: list[FundScenarioResult] = []

        for scenario in scenarios:
            result = self._run_scenario(
                scenario, engines, strategy_capitals, current_nav, hwm,
            )
            results.append(result)

        worst = min(results, key=lambda r: r.pnl_impact) if results else None
        passes = all(r.drawdown_pct < self._max_fund_dd for r in results)

        return FundStressResult(
            timestamp=time.time(),
            scenarios=results,
            worst_scenario=worst.scenario.name if worst else "",
            worst_pnl=worst.pnl_impact if worst else 0.0,
            current_nav=current_nav,
            passes_all=passes,
        )

    def _run_scenario(
        self,
        scenario: FundScenario,
        engines: dict[str, Any],
        strategy_capitals: dict[str, float],
        current_nav: float,
        hwm: float,
    ) -> FundScenarioResult:
        """Evaluate a single scenario against all strategies."""
        total_impact = 0.0
        strategy_impacts: dict[str, float] = {}
        breached: list[str] = []

        for name, engine in engines.items():
            capital = strategy_capitals.get(name, 0.0)
            impact = self._strategy_impact(scenario, engine, capital)
            strategy_impacts[name] = impact
            total_impact += impact

            # Check per-strategy drawdown
            strat_dd = abs(impact / capital * 100.0) if capital > 0 else 0.0
            if strat_dd >= self._max_strat_dd:
                breached.append(name)

        nav_after = current_nav + total_impact
        dd_pct = ((hwm - nav_after) / hwm * 100.0) if hwm > 0 else 0.0

        worst_name = min(strategy_impacts, key=strategy_impacts.get) if strategy_impacts else ""
        worst_pnl = strategy_impacts.get(worst_name, 0.0)

        return FundScenarioResult(
            scenario=scenario,
            pnl_impact=total_impact,
            nav_after=nav_after,
            drawdown_pct=dd_pct,
            strategies_breached=breached,
            worst_strategy=worst_name,
            worst_strategy_pnl=worst_pnl,
            details=strategy_impacts,
        )

    @staticmethod
    def _strategy_impact(
        scenario: FundScenario,
        engine: Any,
        capital: float,
    ) -> float:
        """Estimate P&L impact of a scenario on a single strategy."""
        impact = 0.0

        try:
            positions = engine.positions()
        except Exception:
            # If we can't read positions, assume capital-proportional impact
            return capital * (scenario.price_shock_pct / 100.0)

        if not positions:
            return 0.0

        # Apply price shock to all positions
        for pos in positions:
            # Prefer current mark price from feed snapshot
            mark_price = pos.avg_entry_price  # fallback
            try:
                snap = engine.feed_snapshot(pos.market_id)
                if snap is not None and hasattr(snap, 'price') and snap.price > 0:
                    mark_price = snap.price
            except Exception:
                pass
            notional = abs(pos.size * mark_price)
            impact += notional * (scenario.price_shock_pct / 100.0)

        # Exchange outage: assume positions on that exchange cannot be hedged
        if scenario.exchange_down:
            for pos in positions:
                exchange = getattr(pos, "exchange", "")
                if exchange == scenario.exchange_down:
                    # Additional 5% adverse move for unhedgeable positions
                    notional = abs(pos.size * pos.avg_entry_price)
                    impact -= notional * 0.05

        # Liquidity reduction: slippage on exit
        if scenario.liquidity_reduction_pct > 0:
            for pos in positions:
                notional = abs(pos.size * pos.avg_entry_price)
                slippage = notional * (scenario.liquidity_reduction_pct / 100.0) * 0.05
                impact -= slippage

        return impact
