"""Pre-trade risk guard — intercepts orders before broker submission.

Inspired by NautilusTrader's RiskEngine. Every order passes through the
guard before reaching the broker adapter. Any violation raises
``RiskGuardViolation`` and the order is rejected.
"""

from __future__ import annotations

import time
from dataclasses import dataclass, field
from typing import Any, Dict, FrozenSet, List, Optional, Set

from ..common.errors import CPZError
from .models import OrderSubmitRequest


class RiskGuardViolation(CPZError):
    """Raised when an order violates a pre-trade risk rule."""

    def __init__(self, rule: str, message: str, order: Optional[OrderSubmitRequest] = None) -> None:
        self.rule = rule
        self.order = order
        super().__init__(f"Risk guard [{rule}]: {message}")


@dataclass
class RiskGuardConfig:
    """Configuration for pre-trade risk checks.

    Set any field to ``None`` to disable that check.
    """

    max_order_value: Optional[float] = None
    max_order_qty: Optional[float] = None
    max_position_pct: Optional[float] = None
    max_notional_exposure: Optional[float] = None
    max_daily_loss: Optional[float] = None
    max_open_orders: Optional[int] = None
    blocked_symbols: FrozenSet[str] = field(default_factory=frozenset)
    allowed_symbols: Optional[FrozenSet[str]] = None
    cooldown_seconds: Optional[float] = None
    max_orders_per_minute: Optional[int] = None
    enabled: bool = True


class RiskGuard:
    """Pre-trade risk engine that validates orders before submission.

    Attach to a ``BrokerRouter`` to enforce limits on every order::

        guard = RiskGuard(
            max_order_value=50_000,
            max_position_pct=0.10,
            max_daily_loss=-5_000,
            blocked_symbols=["GME", "AMC"],
        )
        router.set_risk_guard(guard)

    Or from a strategy::

        client.execution.set_risk_guard(
            max_order_value=50_000,
            max_daily_loss=-5_000,
        )
    """

    def __init__(
        self,
        max_order_value: float | None = None,
        max_order_qty: float | None = None,
        max_position_pct: float | None = None,
        max_notional_exposure: float | None = None,
        max_daily_loss: float | None = None,
        max_open_orders: int | None = None,
        blocked_symbols: set[str] | frozenset[str] | list[str] | None = None,
        allowed_symbols: set[str] | frozenset[str] | list[str] | None = None,
        cooldown_seconds: float | None = None,
        max_orders_per_minute: int | None = None,
        enabled: bool = True,
    ) -> None:
        self.config = RiskGuardConfig(
            max_order_value=max_order_value,
            max_order_qty=max_order_qty,
            max_position_pct=max_position_pct,
            max_notional_exposure=max_notional_exposure,
            max_daily_loss=max_daily_loss,
            max_open_orders=max_open_orders,
            blocked_symbols=frozenset(blocked_symbols or []),
            allowed_symbols=frozenset(allowed_symbols) if allowed_symbols else None,
            cooldown_seconds=cooldown_seconds,
            max_orders_per_minute=max_orders_per_minute,
            enabled=enabled,
        )

        self._daily_pnl: float = 0.0
        self._daily_reset_date: str = time.strftime("%Y-%m-%d")
        self._open_order_count: int = 0
        self._last_order_time: Dict[str, float] = {}
        self._order_timestamps: List[float] = []
        self._total_notional: float = 0.0
        self._halted = False
        self._violations: List[RiskGuardViolation] = []

    @property
    def is_halted(self) -> bool:
        return self._halted

    @property
    def violations(self) -> List[RiskGuardViolation]:
        return list(self._violations)

    def validate(
        self,
        req: OrderSubmitRequest,
        equity: float = 0.0,
        current_price: float | None = None,
    ) -> None:
        """Validate an order against all configured rules.

        Raises ``RiskGuardViolation`` on the first rule breach.
        """
        if not self.config.enabled:
            return

        if self._halted:
            raise RiskGuardViolation("halted", "Trading halted by risk guard", req)

        today = time.strftime("%Y-%m-%d")
        if today != self._daily_reset_date:
            self._daily_pnl = 0.0
            self._daily_reset_date = today
            self._order_timestamps.clear()

        symbol = req.symbol.upper()

        if self.config.blocked_symbols and symbol in {s.upper() for s in self.config.blocked_symbols}:
            self._record_violation("blocked_symbol", f"{symbol} is blocked", req)

        if self.config.allowed_symbols is not None:
            allowed = {s.upper() for s in self.config.allowed_symbols}
            if symbol not in allowed:
                self._record_violation("allowed_symbols", f"{symbol} not in allowed list", req)

        est_price = current_price or 0.0
        est_qty = req.qty or (req.notional / est_price if req.notional and est_price else 0)
        est_value = est_qty * est_price if est_price else (req.notional or 0)

        if self.config.max_order_qty and est_qty > self.config.max_order_qty:
            self._record_violation(
                "max_order_qty",
                f"qty {est_qty} exceeds max {self.config.max_order_qty}",
                req,
            )

        if self.config.max_order_value and est_value > self.config.max_order_value:
            self._record_violation(
                "max_order_value",
                f"value ${est_value:,.2f} exceeds max ${self.config.max_order_value:,.2f}",
                req,
            )

        if self.config.max_position_pct and equity > 0 and est_value > 0:
            pct = est_value / equity
            if pct > self.config.max_position_pct:
                self._record_violation(
                    "max_position_pct",
                    f"position {pct:.1%} exceeds max {self.config.max_position_pct:.1%}",
                    req,
                )

        if self.config.max_notional_exposure and (self._total_notional + est_value) > self.config.max_notional_exposure:
            self._record_violation(
                "max_notional_exposure",
                f"total notional ${self._total_notional + est_value:,.2f} "
                f"exceeds max ${self.config.max_notional_exposure:,.2f}",
                req,
            )

        if self.config.max_open_orders and self._open_order_count >= self.config.max_open_orders:
            self._record_violation(
                "max_open_orders",
                f"{self._open_order_count} open orders (max {self.config.max_open_orders})",
                req,
            )

        if self.config.max_daily_loss and self._daily_pnl < self.config.max_daily_loss:
            self._halted = True
            self._record_violation(
                "max_daily_loss",
                f"daily P&L ${self._daily_pnl:,.2f} breached limit ${self.config.max_daily_loss:,.2f}",
                req,
            )

        now = time.time()
        if self.config.cooldown_seconds:
            last = self._last_order_time.get(symbol, 0)
            elapsed = now - last
            if elapsed < self.config.cooldown_seconds:
                self._record_violation(
                    "cooldown",
                    f"cooldown: {elapsed:.1f}s since last {symbol} order (min {self.config.cooldown_seconds}s)",
                    req,
                )

        if self.config.max_orders_per_minute:
            cutoff = now - 60
            self._order_timestamps = [t for t in self._order_timestamps if t > cutoff]
            if len(self._order_timestamps) >= self.config.max_orders_per_minute:
                self._record_violation(
                    "max_orders_per_minute",
                    f"{len(self._order_timestamps)} orders in last 60s (max {self.config.max_orders_per_minute})",
                    req,
                )

        self._last_order_time[symbol] = now
        self._order_timestamps.append(now)

    def record_fill(self, pnl: float, notional: float = 0.0) -> None:
        """Record a fill for daily P&L and exposure tracking."""
        self._daily_pnl += pnl
        self._total_notional += notional

    def record_order_opened(self) -> None:
        self._open_order_count += 1

    def record_order_closed(self) -> None:
        self._open_order_count = max(0, self._open_order_count - 1)

    def reset_halt(self) -> None:
        self._halted = False

    def reset_daily(self) -> None:
        self._daily_pnl = 0.0
        self._halted = False
        self._order_timestamps.clear()

    def _record_violation(self, rule: str, message: str, req: OrderSubmitRequest) -> None:
        v = RiskGuardViolation(rule, message, req)
        self._violations.append(v)
        raise v
