# app/src/trading/execution/risk_close.py

from __future__ import annotations

from typing import Protocol

from src.core.numbers import safe_float
from src.core.types import NumericLike
from src.trading.auto.state import AutoTradeState
from src.trading.execution.models import ExecutionDecision
from src.trading.execution.pricing import ExecutionPrice
from src.trading.position.state import PositionState
from src.trading.execution.position_metrics import build_position_metrics


class _ExecutionRiskCloseProtocol(Protocol):
    _position: PositionState

    # получить цену выхода для стороны позиции
    def _exit_price_for_side(
        self,
        symbol: str,
        side: str,
    ) -> ExecutionPrice:
        ...

    # закрыть открытую позицию
    def _close_position(
        self,
        state: AutoTradeState,
        *,
        forced_reason: str | None = None,
        forced_exit_price: NumericLike | None = None,
        forced_pnl: NumericLike | None = None,
        forced_price_meta: ExecutionPrice | None = None,
    ) -> ExecutionDecision:
        ...


class ExecutionRiskCloseMixin(_ExecutionRiskCloseProtocol):
    # закрыть позицию по risk-правилу с уже рассчитанными ценой и PnL
    def _close_position_by_risk(
        self,
        state: AutoTradeState,
        *,
        reason: str,
        current_price: NumericLike | None,
        unrealized_pnl: NumericLike | None,
        current_execution: ExecutionPrice,
    ) -> ExecutionDecision:
        return self._close_position(
            state,
            forced_reason=reason,
            forced_exit_price=current_price,
            forced_pnl=unrealized_pnl,
            forced_price_meta=current_execution,
        )

    # проверить, нужно ли закрыть позицию по max loss / stop loss / take profit
    def _risk_close_decision(self, state: AutoTradeState) -> ExecutionDecision | None:
        position = type(self)._position

        if position.side == "NONE":
            return None

        try:
            current_execution = self._exit_price_for_side(
                position.symbol or state.symbol,
                position.side,
            )
            current_price = current_execution.price
        except Exception:
            return None

        metrics = build_position_metrics(
            position,
            current_price=current_price,
        )

        price_move_percent = metrics.price_move_percent
        unrealized_pnl = metrics.net_pnl_usd

        if self._is_max_loss_hit(state, unrealized_pnl):
            return self._close_position_by_risk(
                state,
                reason="MAX_LOSS",
                current_price=current_price,
                unrealized_pnl=unrealized_pnl,
                current_execution=current_execution,
            )

        if self._is_stop_loss_hit(state, price_move_percent):
            return self._close_position_by_risk(
                state,
                reason="STOP_LOSS",
                current_price=current_price,
                unrealized_pnl=unrealized_pnl,
                current_execution=current_execution,
            )

        if self._is_take_profit_hit(state, unrealized_pnl):
            return self._close_position_by_risk(
                state,
                reason="TAKE_PROFIT",
                current_price=current_price,
                unrealized_pnl=unrealized_pnl,
                current_execution=current_execution,
            )

        return None

    # проверить, достигнут ли stop loss в процентах
    def _is_stop_loss_hit(
        self,
        state: AutoTradeState,
        price_move_percent: NumericLike | None,
    ) -> bool:
        stop_loss_percent = safe_float(state.stop_loss_percent)
        price_move = safe_float(price_move_percent)

        if stop_loss_percent is None or stop_loss_percent <= 0:
            return False

        if price_move is None:
            return False

        return price_move <= -abs(stop_loss_percent)

    # проверить, достигнут ли take profit в процентах
    def _is_take_profit_hit(
        self,
        state: AutoTradeState,
        unrealized_pnl: NumericLike | None,
    ) -> bool:
        take_profit_percent = safe_float(state.take_profit_percent)
        pnl = safe_float(unrealized_pnl)

        if take_profit_percent is None or take_profit_percent <= 0:
            return False

        if pnl is None:
            return False

        position = type(self)._position

        entry_price = safe_float(position.entry_price)
        size = safe_float(position.size)

        if entry_price is None or entry_price <= 0:
            return False

        if size is None or size <= 0:
            return False

        target_profit_usd = abs(entry_price * size * (take_profit_percent / 100))

        return pnl >= target_profit_usd

    # проверить, достигнут ли максимальный убыток в USD
    def _is_max_loss_hit(
        self,
        state: AutoTradeState,
        unrealized_pnl: NumericLike | None,
    ) -> bool:
        max_loss_usd = safe_float(state.max_loss_usd)
        pnl = safe_float(unrealized_pnl)

        if max_loss_usd is None or max_loss_usd <= 0:
            return False

        if pnl is None:
            return False

        return pnl <= -abs(max_loss_usd)