"""Layer 1: Stateless risk evaluation engine."""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, Callable, Protocol

from pyfortis._types import (
    BreachAction,
    BreachRecord,
    CircuitBreakerSpec,
    CircuitBreakerType,
    EscalationPolicy,
    Exposure,
    GateResult,
    GateVerdict,
    LimitCheckResult,
    LimitSpec,
    LimitScope,
    MetricResult,
    MetricSpec,
    Order,
    Position,
    Severity,
)
from pyfortis.errors import RiskPolicy, ValidatorNotFoundError, CalculatorNotFoundError


class ValidatorFn(Protocol):
    def __call__(
        self,
        limit: LimitSpec,
        order: Order,
        positions: dict[str, Position],
        exposures: dict[str, Exposure],
        context: dict[str, Any],
    ) -> LimitCheckResult: ...


class CalculatorFn(Protocol):
    def __call__(
        self,
        metric: MetricSpec,
        positions: dict[str, Position],
        returns_data: list[float],
        context: dict[str, Any],
    ) -> MetricResult: ...


class _ValidatorChecker:
    """Thin wrapper for validator dispatch, injected by RiskEngine."""

    def __init__(self, get_fn: Callable[[str], ValidatorFn | None]) -> None:
        self._get = get_fn

    def check(
        self,
        limit: LimitSpec,
        order: Order,
        positions: dict[str, Position],
        exposures: dict[str, Exposure],
        context: dict[str, Any],
    ) -> LimitCheckResult:
        fn = self._get(limit.type.value)
        if fn is None:
            raise ValidatorNotFoundError(
                f"No validator for limit type '{limit.type.value}'",
                context={"limit_name": limit.name, "limit_type": limit.type.value},
            )
        return fn(limit, order, positions, exposures, context)


class _CalculatorChecker:
    """Thin wrapper for calculator dispatch, injected by RiskEngine."""

    def __init__(self, get_fn: Callable[[str], CalculatorFn | None]) -> None:
        self._get = get_fn

    def compute(
        self,
        metric: MetricSpec,
        positions: dict[str, Position],
        returns_data: list[float],
        context: dict[str, Any],
    ) -> MetricResult:
        fn = self._get(metric.type.value)
        if fn is None:
            raise CalculatorNotFoundError(
                f"No calculator for metric type '{metric.type.value}'",
                context={"metric_name": metric.name, "metric_type": metric.type.value},
            )
        return fn(metric, positions, returns_data, context)


class Engine:
    """Stateless risk evaluation engine (Layer 1).

    Evaluates orders against limits, computes metrics, and checks
    circuit breakers. All state (positions, exposures) is passed in.
    """

    def __init__(
        self,
        limits: list[LimitSpec],
        metrics: list[MetricSpec],
        circuit_breakers: list[CircuitBreakerSpec],
        escalation_policy: EscalationPolicy,
        meta: dict[str, Any] | None = None,
        risk_policy: RiskPolicy | None = None,
        validator_checker: _ValidatorChecker | None = None,
        calculator_checker: _CalculatorChecker | None = None,
    ) -> None:
        self.limits = limits
        self.metrics = metrics
        self.circuit_breakers = circuit_breakers
        self.escalation_policy = escalation_policy
        self.meta = meta or {}
        self.risk_policy = risk_policy or RiskPolicy()
        self._validator_checker = validator_checker
        self._calculator_checker = calculator_checker

        # Build indexes for efficient lookup
        self._limits_by_name: dict[str, LimitSpec] = {l.name: l for l in limits}
        self._limits_by_scope: dict[LimitScope, list[LimitSpec]] = {}
        for limit in limits:
            self._limits_by_scope.setdefault(limit.scope, []).append(limit)

        self._metrics_by_name: dict[str, MetricSpec] = {m.name: m for m in metrics}
        self._cb_by_name: dict[str, CircuitBreakerSpec] = {
            cb.name: cb for cb in circuit_breakers
        }

    def evaluate_order(
        self,
        order: Order,
        positions: dict[str, Position],
        exposures: dict[str, Exposure],
        context: dict[str, Any] | None = None,
    ) -> GateResult:
        """Evaluate an order against all enabled limits."""
        context = context or {}
        checks: list[LimitCheckResult] = []
        breaches: list[BreachRecord] = []
        max_severity = Severity.INFO

        for limit in self.limits:
            if not limit.enabled:
                continue

            if self._validator_checker is None:
                raise ValidatorNotFoundError(
                    "No validator checker configured",
                    context={"limit_name": limit.name},
                )

            try:
                result = self._validator_checker.check(
                    limit, order, positions, exposures, context
                )
            except ValidatorNotFoundError:
                if self.risk_policy.strict_mode:
                    raise
                if self.risk_policy.fail_open:
                    result = LimitCheckResult(
                        limit_name=limit.name,
                        passed=True,
                        severity=limit.severity,
                        message=f"Validator not found for {limit.type.value}, fail_open=True",
                    )
                else:
                    result = LimitCheckResult(
                        limit_name=limit.name,
                        passed=False,
                        severity=limit.severity,
                        message=f"Validator not found for {limit.type.value}, fail_open=False",
                    )

            checks.append(result)

            if not result.passed:
                if result.severity.rank > max_severity.rank:
                    max_severity = result.severity

                actions = self._resolve_actions(result.severity)
                breaches.append(
                    BreachRecord(
                        limit_name=limit.name,
                        severity=result.severity,
                        message=result.message,
                        order_id=order.order_id,
                        instrument=order.instrument,
                        actions=actions,
                        details=result.details,
                    )
                )

        return self._determine_verdict(order.order_id, checks, breaches, max_severity)

    def compute_metric(
        self,
        metric: MetricSpec,
        positions: dict[str, Position],
        returns_data: list[float],
        context: dict[str, Any] | None = None,
    ) -> MetricResult:
        """Compute a single metric."""
        if self._calculator_checker is None:
            raise CalculatorNotFoundError(
                "No calculator checker configured",
                context={"metric_name": metric.name},
            )
        return self._calculator_checker.compute(
            metric, positions, returns_data, context or {}
        )

    def compute_all_metrics(
        self,
        positions: dict[str, Position],
        returns_data: list[float],
        context: dict[str, Any] | None = None,
    ) -> list[MetricResult]:
        """Compute all enabled metrics."""
        results: list[MetricResult] = []
        for metric in self.metrics:
            if not metric.enabled:
                continue
            results.append(
                self.compute_metric(metric, positions, returns_data, context)
            )
        return results

    def evaluate_circuit_breakers(
        self,
        daily_pnl: float = 0.0,
        peak_value: float = 0.0,
        current_value: float = 0.0,
        volatility: float = 0.0,
    ) -> list[CircuitBreakerSpec]:
        """Check all circuit breakers, return list of tripped ones."""
        tripped: list[CircuitBreakerSpec] = []

        for cb in self.circuit_breakers:
            if cb.type is CircuitBreakerType.DAILY_PNL:
                max_loss = cb.params.get("max_loss", float("-inf"))
                if daily_pnl <= max_loss:
                    tripped.append(cb)

            elif cb.type is CircuitBreakerType.DRAWDOWN:
                max_dd = cb.params.get("max_drawdown", 1.0)
                if peak_value > 0:
                    dd = (peak_value - current_value) / peak_value
                    if dd >= max_dd:
                        tripped.append(cb)

            elif cb.type is CircuitBreakerType.VOLATILITY:
                max_vol = cb.params.get("max_volatility", float("inf"))
                if volatility >= max_vol:
                    tripped.append(cb)

        return tripped

    def _resolve_actions(self, severity: Severity) -> list[BreachAction]:
        """Resolve breach actions from escalation policy."""
        actions = self.escalation_policy.escalation.get(severity)
        if actions:
            return list(actions)
        return [self.escalation_policy.default_action]

    def _determine_verdict(
        self,
        order_id: str,
        checks: list[LimitCheckResult],
        breaches: list[BreachRecord],
        max_severity: Severity,
    ) -> GateResult:
        """Determine gate verdict based on severity of breaches."""
        if not breaches:
            return GateResult.approved(order_id, checks)

        if max_severity.rank >= Severity.CRITICAL.rank:
            return GateResult.rejected(
                order_id,
                checks,
                breaches,
                message=f"Order rejected: {max_severity.value} breach",
            )

        return GateResult.approved_with_warning(
            order_id,
            checks,
            breaches,
            message=f"Order approved with {len(breaches)} warning(s)",
        )
