from __future__ import annotations

from dataclasses import dataclass
from decimal import Decimal

from .futures_models import (
    FuturesAction,
    FuturesPosition,
    FuturesPositionSide,
    FuturesPrediction,
    FuturesRiskDecision,
    FuturesSymbolMetadata,
)


@dataclass(frozen=True)
class FuturesRiskSettings:
    margin_amount: Decimal
    default_leverage: int
    max_leverage: int
    max_total_margin: Decimal
    min_confidence: Decimal
    daily_realized_loss_limit: Decimal
    max_margin_loss_percent: Decimal
    min_liquidation_buffer_percent: Decimal
    min_stop_distance_percent: Decimal
    max_stop_distance_percent: Decimal
    min_take_profit_distance_percent: Decimal
    max_take_profit_distance_percent: Decimal


@dataclass(frozen=True)
class FuturesRiskContext:
    prediction: FuturesPrediction
    metadata: FuturesSymbolMetadata
    position: FuturesPosition
    mark_price: Decimal
    total_isolated_margin: Decimal
    daily_realized_pnl: Decimal
    hedge_mode: bool


class FuturesRiskEngine:
    def __init__(self, settings: FuturesRiskSettings) -> None:
        self.settings = settings

    def evaluate(self, context: FuturesRiskContext) -> FuturesRiskDecision:
        prediction = context.prediction
        if not self._valid_base_inputs(context):
            return FuturesRiskDecision(False, "invalid risk input", prediction)
        if context.hedge_mode:
            return FuturesRiskDecision(False, "hedge mode active", prediction)
        if prediction.confidence < self.settings.min_confidence:
            return FuturesRiskDecision(False, "confidence below minimum", prediction)
        if prediction.symbol != context.metadata.symbol:
            return FuturesRiskDecision(False, "metadata symbol mismatch", prediction)
        if prediction.action is FuturesAction.HOLD:
            return FuturesRiskDecision(False, "hold signal", prediction)
        if prediction.action is FuturesAction.CLOSE:
            return self._evaluate_close(context)
        if context.daily_realized_pnl <= -self.settings.daily_realized_loss_limit:
            return FuturesRiskDecision(
                False,
                "daily net loss limit reached",
                prediction,
            )
        return self._evaluate_open(context)

    def _evaluate_close(self, context: FuturesRiskContext) -> FuturesRiskDecision:
        prediction = context.prediction
        if context.position.side is FuturesPositionSide.FLAT:
            return FuturesRiskDecision(False, "no position to close", prediction)
        if prediction.close_percent not in {25, 50, 100}:
            return FuturesRiskDecision(False, "invalid close percent", prediction)
        return FuturesRiskDecision(True, "approved", prediction)

    def _evaluate_open(self, context: FuturesRiskContext) -> FuturesRiskDecision:
        prediction = context.prediction
        if context.position.side is not FuturesPositionSide.FLAT:
            if (
                prediction.action is FuturesAction.OPEN_LONG
                and context.position.side is FuturesPositionSide.LONG
            ) or (
                prediction.action is FuturesAction.OPEN_SHORT
                and context.position.side is FuturesPositionSide.SHORT
            ):
                return FuturesRiskDecision(
                    False,
                    "same-direction position exists",
                    prediction,
                )
            return FuturesRiskDecision(
                False,
                "opposite position requires close first",
                prediction,
            )

        leverage = self._resolve_leverage(prediction)
        if leverage is None:
            return FuturesRiskDecision(False, "invalid risk input", prediction)
        notional = self.settings.margin_amount * Decimal(leverage)
        if (
            context.total_isolated_margin + self.settings.margin_amount
            > self.settings.max_total_margin
        ):
            return FuturesRiskDecision(False, "total margin above max", prediction)
        if notional < context.metadata.min_notional:
            return FuturesRiskDecision(False, "notional below min notional", prediction)
        if prediction.stop_loss_price is None or prediction.take_profit_price is None:
            return FuturesRiskDecision(False, "missing protection prices", prediction)
        if not self._valid_protection_prices(prediction):
            return FuturesRiskDecision(False, "invalid risk input", prediction)
        if not self._valid_protection_direction(prediction, context.mark_price):
            return FuturesRiskDecision(
                False,
                "invalid stop or take-profit direction",
                prediction,
            )
        if not self._distance_in_range(
            context.mark_price,
            prediction.stop_loss_price,
            self.settings.min_stop_distance_percent,
            self.settings.max_stop_distance_percent,
        ):
            return FuturesRiskDecision(False, "stop distance outside limits", prediction)
        if not self._distance_in_range(
            context.mark_price,
            prediction.take_profit_price,
            self.settings.min_take_profit_distance_percent,
            self.settings.max_take_profit_distance_percent,
        ):
            return FuturesRiskDecision(
                False,
                "take-profit distance outside limits",
                prediction,
            )
        if (
            self._estimated_liquidation_buffer_percent(leverage)
            <= self.settings.min_liquidation_buffer_percent
        ):
            return FuturesRiskDecision(
                False,
                "estimated liquidation buffer below minimum",
                prediction,
            )
        stop_loss_amount = (
            notional
            * self._distance_percent(context.mark_price, prediction.stop_loss_price)
            / Decimal("100")
        )
        max_loss_amount = (
            self.settings.margin_amount
            * self.settings.max_margin_loss_percent
            / Decimal("100")
        )
        if stop_loss_amount > max_loss_amount:
            return FuturesRiskDecision(
                False,
                "estimated stop loss above margin risk budget",
                prediction,
            )
        return FuturesRiskDecision(
            True,
            "approved",
            prediction,
            leverage=leverage,
            margin_amount=self.settings.margin_amount,
            notional=notional,
        )

    def _valid_base_inputs(self, context: FuturesRiskContext) -> bool:
        decimal_values = (
            context.prediction.confidence,
            context.mark_price,
            context.total_isolated_margin,
            context.daily_realized_pnl,
            context.metadata.min_notional,
            context.position.quantity,
            self.settings.margin_amount,
            self.settings.max_total_margin,
            self.settings.min_confidence,
            self.settings.daily_realized_loss_limit,
            self.settings.max_margin_loss_percent,
            self.settings.min_liquidation_buffer_percent,
            self.settings.min_stop_distance_percent,
            self.settings.max_stop_distance_percent,
            self.settings.min_take_profit_distance_percent,
            self.settings.max_take_profit_distance_percent,
        )
        if not all(self._finite_decimal(value) for value in decimal_values):
            return False
        if context.mark_price <= 0 or context.total_isolated_margin < 0:
            return False
        if context.metadata.min_notional < 0 or self.settings.margin_amount <= 0:
            return False
        if self.settings.max_total_margin < 0:
            return False
        if self.settings.daily_realized_loss_limit < 0:
            return False
        if self.settings.max_margin_loss_percent < 0:
            return False
        if self.settings.min_liquidation_buffer_percent < 0:
            return False
        if (
            self.settings.min_stop_distance_percent
            > self.settings.max_stop_distance_percent
        ):
            return False
        return (
            self.settings.min_take_profit_distance_percent
            <= self.settings.max_take_profit_distance_percent
        )

    def _finite_decimal(self, value: Decimal) -> bool:
        return isinstance(value, Decimal) and value.is_finite()

    def _positive_decimal(self, value: Decimal) -> bool:
        return self._finite_decimal(value) and value > 0

    def _valid_leverage(self, leverage: int) -> bool:
        return (
            isinstance(leverage, int)
            and not isinstance(leverage, bool)
            and leverage > 0
        )

    def _resolve_leverage(self, prediction: FuturesPrediction) -> int | None:
        leverage = (
            self.settings.default_leverage
            if prediction.leverage is None
            else prediction.leverage
        )
        if not self._valid_leverage(leverage):
            return None
        if not self._valid_leverage(self.settings.max_leverage):
            return None
        return min(leverage, self.settings.max_leverage)

    def _valid_protection_prices(self, prediction: FuturesPrediction) -> bool:
        if prediction.stop_loss_price is None or prediction.take_profit_price is None:
            return False
        return self._positive_decimal(
            prediction.stop_loss_price
        ) and self._positive_decimal(prediction.take_profit_price)

    def _valid_protection_direction(
        self,
        prediction: FuturesPrediction,
        mark_price: Decimal,
    ) -> bool:
        if prediction.stop_loss_price is None or prediction.take_profit_price is None:
            return False
        if prediction.action is FuturesAction.OPEN_LONG:
            return prediction.stop_loss_price < mark_price < prediction.take_profit_price
        if prediction.action is FuturesAction.OPEN_SHORT:
            return prediction.take_profit_price < mark_price < prediction.stop_loss_price
        return False

    def _distance_in_range(
        self,
        reference: Decimal,
        price: Decimal | None,
        minimum: Decimal,
        maximum: Decimal,
    ) -> bool:
        if price is None:
            return False
        distance = self._distance_percent(reference, price)
        return minimum <= distance <= maximum

    def _distance_percent(self, reference: Decimal, price: Decimal) -> Decimal:
        return abs(reference - price) / reference * Decimal("100")

    def _estimated_liquidation_buffer_percent(self, leverage: int) -> Decimal:
        return Decimal("100") / Decimal(leverage)
