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

from __future__ import annotations

from typing import Protocol

from src.core.numbers import safe_float
from src.trading.auto.state import AutoTradeState
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 _ExecutionRuntimeProtocol(Protocol):
    _position: PositionState

    def _exit_price_for_side(self, symbol: str, side: str) -> ExecutionPrice: ...

    def _now_time(self) -> str: ...


class ExecutionPositionRuntimeMixin(_ExecutionRuntimeProtocol):
    # получить текущую paper-позицию
    def get_position(self) -> PositionState:
        return type(self)._position

    # Обновить runtime-метрики открытой позиции.
    # Важно: PnL, комиссия, overnight и движение цены теперь считаются
    # один раз через position_metrics.py.
    def _update_unrealized_pnl(self, state: AutoTradeState) -> None:
        position = type(self)._position

        if position.side == "NONE":
            self._sync_state_from_position(state)
            return

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

        if current_price is None or current_price <= 0:
            self._sync_state_from_position(state)
            return

        metrics = build_position_metrics(
            position,
            current_price=current_price,
        )

        position.unrealized_pnl_usd = metrics.net_pnl_usd
        position.updated_at = self._now_time()

        # Единые runtime-метрики позиции.
        # Эти значения дальше используют health/semantics/protection,
        # поэтому не пересчитываем их в других файлах.
        state.position_pnl_percent = metrics.pnl_percent
        state.position_hold_seconds = metrics.hold_seconds

        self._refresh_position_runtime_metrics(
            position=position,
            current_price=current_price,
            price_move_percent=metrics.price_move_percent,
            pnl_percent=metrics.pnl_percent,
            hold_seconds=metrics.hold_seconds,
        )

        self._sync_state_from_position(state)

    # синхронизировать AutoTradeState с текущей paper-позицией
    def _sync_state_from_position(self, state: AutoTradeState) -> None:
        position = type(self)._position

        state.position_side = position.side
        state.entry_price = position.entry_price
        state.position_size = position.size
        state.unrealized_pnl_usd = position.unrealized_pnl_usd

        if position.side == "NONE":
            state.position_opened_monotonic_at = None
            state.position_peak_pnl_usd = None
            state.position_peak_pnl_percent = None
            state.position_mfe_percent = None
            state.position_mae_percent = None
            state.position_fatigue_score = None
            state.position_fatigue_state = None
            state.position_giveback_percent = None
            state.position_conviction_state = None
            state.position_exit_urgency = None
            state.position_reversal_risk = None
            state.position_lifecycle_stage = None
            state.position_hold_quality = None
            state.position_decay_state = None
            state.position_exit_signal = None
            state.position_exit_confidence = None
            state.position_intelligence_reason = None
            state.position_recommended_action = None
            state.position_stall_state = None
            state.position_stall_reason = None
            state.position_pnl_percent = None
            state.position_hold_seconds = None
            state.position_pressure = None
            state.position_health_score = None
            state.position_health_status = None
            state.position_health_reason = None
            state.position_risk_level = None
            state.position_risk_reason = None
            state.position_trend_alignment = None
            state.position_adverse_momentum = False
            state.position_exit_pressure = None
            return

        state.position_opened_monotonic_at = position.opened_monotonic_at
        state.position_peak_pnl_usd = position.peak_unrealized_pnl_usd
        state.position_peak_pnl_percent = position.peak_pnl_percent
        state.position_mfe_percent = position.max_favorable_excursion_percent
        state.position_mae_percent = position.max_adverse_excursion_percent
        state.position_fatigue_score = position.fatigue_score
        state.position_fatigue_state = position.fatigue_state

    # Обновить runtime-память позиции:
    # peak PnL, MFE/MAE, best/worst price, fatigue.
    # Само движение цены уже рассчитано выше через position_metrics.py,
    # поэтому здесь не пересчитываем его повторно.
    def _refresh_position_runtime_metrics(
        self,
        *,
        position: PositionState,
        current_price: float,
        price_move_percent: float,
        pnl_percent: float,
        hold_seconds: int | None,
    ) -> None:
        if price_move_percent is None:
            return

        pnl = safe_float(position.unrealized_pnl_usd)

        if pnl is not None:
            peak_pnl = safe_float(position.peak_unrealized_pnl_usd)

            if peak_pnl is None or pnl > peak_pnl:
                position.peak_unrealized_pnl_usd = pnl

        peak_percent = safe_float(position.peak_pnl_percent)

        if peak_percent is None or pnl_percent > peak_percent:
            position.peak_pnl_percent = pnl_percent

        mfe = safe_float(position.max_favorable_excursion_percent)
        mae = safe_float(position.max_adverse_excursion_percent)

        if mfe is None or price_move_percent > mfe:
            position.max_favorable_excursion_percent = price_move_percent

        if mae is None or price_move_percent < mae:
            position.max_adverse_excursion_percent = price_move_percent

        best_price = safe_float(position.best_price_seen)
        worst_price = safe_float(position.worst_price_seen)

        if best_price is None:
            position.best_price_seen = current_price
        elif position.side == "LONG" and current_price > best_price:
            position.best_price_seen = current_price
        elif position.side == "SHORT" and current_price < best_price:
            position.best_price_seen = current_price

        if worst_price is None:
            position.worst_price_seen = current_price
        elif position.side == "LONG" and current_price < worst_price:
            position.worst_price_seen = current_price
        elif position.side == "SHORT" and current_price > worst_price:
            position.worst_price_seen = current_price

        fatigue_score = self._runtime_fatigue_score(
            position=position,
            current_pnl_percent=price_move_percent,
            hold_seconds=hold_seconds,
        )
        position.fatigue_score = fatigue_score
        position.fatigue_state = self._runtime_fatigue_state(fatigue_score)

    # рассчитать fatigue score позиции
    def _runtime_fatigue_score(
        self,
        *,
        position: PositionState,
        current_pnl_percent: float,
        hold_seconds: int | None,
    ) -> float:
        score = 0.0

        mfe = safe_float(position.max_favorable_excursion_percent) or 0.0
        mae = safe_float(position.max_adverse_excursion_percent) or 0.0

        # Время удержания позиции уже рассчитано централизованно
        # в position_metrics.py, здесь его не пересчитываем.
        resolved_hold_seconds = hold_seconds or 0

        if resolved_hold_seconds >= 1800:
            score += 0.25
        elif resolved_hold_seconds >= 900:
            score += 0.15
        elif resolved_hold_seconds >= 300:
            score += 0.08

        if mfe > 0:
            giveback_ratio = max(
                0.0,
                (mfe - current_pnl_percent) / max(0.01, mfe),
            )

            if giveback_ratio >= 0.75:
                score += 0.25
            elif giveback_ratio >= 0.45:
                score += 0.18
            elif giveback_ratio >= 0.25:
                score += 0.10

        if mae <= -1.0:
            score += 0.25
        elif mae <= -0.5:
            score += 0.15

        return round(max(0.0, min(1.0, score)), 3)

    # преобразовать fatigue score в semantic state
    def _runtime_fatigue_state(self, score: float | None) -> str:
        value = safe_float(score)

        if value is None:
            return "UNKNOWN"

        if value >= 0.75:
            return "EXHAUSTED"

        if value >= 0.50:
            return "TIRED"

        if value >= 0.25:
            return "WATCH"

        return "FRESH"