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

from __future__ import annotations

import math
import time
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.pricing import ExecutionPrice


class _ExecutionSizingProtocol(Protocol):
    _size_precision: int

    # получить цену входа по текущему сигналу
    def _signal_entry_price(
        self,
        state: AutoTradeState,
    ) -> ExecutionPrice:
        ...

    # округлить размер позиции
    def _round_size(
        self,
        size: NumericLike | None,
    ) -> float:
        ...


class ExecutionSizingMixin(_ExecutionSizingProtocol):
    # рассчитать итоговый размер позиции с учётом риска и adaptive multiplier
    def _calculate_position_size(
        self,
        state: AutoTradeState,
        *,
        entry_price: float | None = None,
    ) -> float:
        risk_percent = safe_float(state.risk_percent)
        stop_loss_percent = safe_float(state.stop_loss_percent)
        balance_usd = safe_float(state.allocated_balance_usd) or 0.0

        if risk_percent is None or risk_percent <= 0:
            return self._zero_position_size(state)

        if stop_loss_percent is None or stop_loss_percent <= 0:
            return self._zero_position_size(state)

        price = safe_float(entry_price)

        if price is None:
            try:
                price = safe_float(self._signal_entry_price(state).price)
            except Exception:
                price = None

        if price is None or price <= 0:
            return self._zero_position_size(state)

        target_risk_usd = balance_usd * (risk_percent / 100)
        stop_loss_distance_usd = price * (stop_loss_percent / 100)

        if target_risk_usd <= 0 or stop_loss_distance_usd <= 0:
            return self._zero_position_size(state)

        base_size = target_risk_usd / stop_loss_distance_usd
        multiplier = self._adaptive_size_multiplier(state)
        final_size = base_size * multiplier

        self._sync_adaptive_size_state(
            state,
            base_size=base_size,
            final_size=final_size,
            multiplier=multiplier,
        )

        return self._round_size(final_size)

    # единый выход из расчёта size, когда вход невозможен:
    # сбрасывает adaptive size/runtime risk в 0 и возвращает 0.0
    def _zero_position_size(self, state: AutoTradeState) -> float:
        self._sync_adaptive_size_state(
            state,
            base_size=0.0,
            final_size=0.0,
            multiplier=0.0,
        )
        return 0.0

    # рассчитать коэффициент изменения размера позиции по итоговым runtime/context факторам
    def _adaptive_size_multiplier(self, state: AutoTradeState) -> float:
        multiplier = 1.0

        # execution_confidence_score — итоговая готовность входа:
        # сигнал + подтверждение + рынок + качество исполнения.
        # Если он ниже required_score, размер должен быть 0.
        execution_score = safe_float(
            getattr(state, "execution_confidence_score", None)
        )
        required_score = (
            safe_float(getattr(state, "execution_confidence_required_score", None))
            or 0.65
        )

        if execution_score is not None:
            execution_score = max(0.0, min(1.0, execution_score))

            if execution_score < required_score:
                multiplier *= 0.0
            elif execution_score < 0.75:
                multiplier *= 0.90
            elif execution_score < 0.85:
                multiplier *= 1.00
            else:
                multiplier *= 1.10

        # market_score — новая общая оценка рынка 0..100.
        # Она должна влиять на размер позиции напрямую,
        # но без повторного ручного штрафования по trend/phase/momentum.
        market_score = self._market_score_for_sizing(state)

        if market_score is not None:
            if market_score < 35:
                multiplier *= 0.0
            elif market_score < 55:
                multiplier *= 0.65
            elif market_score < 75:
                multiplier *= 0.85
            elif market_score < 90:
                multiplier *= 1.00
            else:
                multiplier *= 1.12

        execution_quality = getattr(state, "execution_quality", None)
        execution_quality_reason = getattr(
            state,
            "execution_quality_reason",
            None,
        )

        # Качество исполнения оставляем отдельным фактором,
        # потому что оно связано не с рынком, а с возможностью нормально войти:
        # spread, snapshot age, стакан, деградация live-данных.
        if execution_quality == "BLOCKED":
            multiplier *= 0.0
        elif execution_quality == "WARNING":
            if execution_quality_reason == "WIDE_SPREAD":
                multiplier *= 0.75
            elif execution_quality_reason == "AGING_SNAPSHOT":
                multiplier *= 0.85
            elif execution_quality_reason == "SNAPSHOT_UNAVAILABLE":
                multiplier *= 0.70
            else:
                multiplier *= 0.85

        if getattr(state, "market_runtime_degraded", False):
            multiplier *= 0.75

        return round(max(0.0, min(1.25, multiplier)), 4)

    # получить market_score для sizing.
    # Основной источник — state.market_score / state.market_score_percent.
    # Fallback — market_score из execution_confidence_factors, где он хранится как 0..1.
    def _market_score_for_sizing(self, state: AutoTradeState) -> float | None:
        direct_score = safe_float(getattr(state, "market_score", None))

        if direct_score is None:
            direct_score = safe_float(getattr(state, "market_score_percent", None))

        if direct_score is not None:
            return max(0.0, min(100.0, direct_score))

        factors = getattr(state, "execution_confidence_factors", None)

        if isinstance(factors, dict):
            factor_score = safe_float(factors.get("market_score"))

            if factor_score is not None:
                # Старый market_score внутри execution_confidence_factors хранится как 0..1.
                if factor_score <= 1.0:
                    factor_score *= 100

                return max(0.0, min(100.0, factor_score))

        return None

    # синхронизировать рассчитанный adaptive size в AutoTradeState
    def _sync_adaptive_size_state(
        self,
        state: AutoTradeState,
        *,
        base_size: float,
        final_size: float,
        multiplier: float,
    ) -> None:
        reason = self._adaptive_size_reason(multiplier)

        state.adaptive_size_base = self._round_size(base_size)
        state.adaptive_size_final = self._round_size(final_size)
        state.adaptive_size_multiplier = multiplier

        if multiplier != 1:
            state.adaptive_size_changed_at = time.monotonic()

        base_risk_percent = safe_float(state.risk_percent) or 0.0

        state.effective_risk_percent = round(
            base_risk_percent * multiplier,
            4,
        )

        allocated_balance = safe_float(state.allocated_balance_usd) or 0.0

        state.effective_target_risk_usd = round(
            allocated_balance * (state.effective_risk_percent / 100),
            4,
        )

        state.adaptive_size_reason = reason
        state.adaptive_size_factors = {
            "market_score": self._market_score_for_sizing(state),
            "execution_confidence_score": getattr(
                state,
                "execution_confidence_score",
                None,
            ),
            "execution_confidence_level": getattr(
                state,
                "execution_confidence_level",
                None,
            ),
            "market_state": getattr(state, "market_state", None),
            "market_trend_strength": getattr(
                state,
                "market_trend_strength",
                None,
            ),
            "market_trend_quality": getattr(
                state,
                "market_trend_quality",
                None,
            ),
            "market_phase": getattr(state, "market_phase", None),
            "momentum_state": getattr(state, "momentum_state", None),
            "momentum_direction": getattr(
                state,
                "momentum_direction",
                None,
            ),
            "momentum_strength": getattr(
                state,
                "momentum_strength",
                None,
            ),
            "execution_quality": getattr(state, "execution_quality", None),
            "execution_quality_reason": getattr(
                state,
                "execution_quality_reason",
                None,
            ),
            "spread_percent": getattr(state, "spread_percent", None),
            "base_size": self._round_size(base_size),
            "final_size": self._round_size(final_size),
            "multiplier": multiplier,
        }

        if multiplier <= 0:
            state.execution_size_adjustment_reason = "ADAPTIVE_SIZE_ZERO"
        elif multiplier < 1:
            state.execution_size_adjustment_reason = "ADAPTIVE_SIZE_REDUCED"
        elif multiplier > 1:
            state.execution_size_adjustment_reason = "ADAPTIVE_SIZE_INCREASED"
        else:
            state.execution_size_adjustment_reason = None

    # пересчитать effective risk после ограничения размера по margin limit
    def _sync_effective_risk_after_margin_limit(
        self,
        state: AutoTradeState,
        *,
        base_size: float,
        final_size: float,
    ) -> None:
        adaptive_final = safe_float(state.adaptive_size_final) or 0.0

        if adaptive_final <= 0:
            state.effective_risk_percent = 0.0
            state.effective_target_risk_usd = 0.0
            return

        # Фиксируем итоговый size после margin limit, чтобы UI и journal не показывали старое значение.
        state.adaptive_size_final = self._round_size(final_size)

        if state.adaptive_size_factors is not None:
            state.adaptive_size_factors["final_size"] = self._round_size(final_size)
            state.adaptive_size_factors["margin_limited"] = final_size < adaptive_final

        margin_ratio = max(
            0.0,
            min(1.0, final_size / adaptive_final),
        )

        current_effective_risk = safe_float(
            state.effective_risk_percent
        ) or 0.0

        state.effective_risk_percent = round(
            current_effective_risk * margin_ratio,
            4,
        )

        allocated_balance = safe_float(state.allocated_balance_usd) or 0.0

        state.effective_target_risk_usd = round(
            allocated_balance * (state.effective_risk_percent / 100),
            4,
        )

    # вернуть текстовую причину изменения adaptive size
    def _adaptive_size_reason(self, multiplier: float) -> str:
        if multiplier <= 0:
            return "adaptive size заблокировал вход"

        if multiplier < 0.75:
            return "размер позиции сильно уменьшен по risk/runtime факторам"

        if multiplier < 1:
            return "размер позиции умеренно уменьшен по risk/runtime факторам"

        if multiplier > 1:
            return "размер позиции увеличен при сильном execution context"

        return "размер позиции без adaptive корректировки"

    # ограничить размер позиции по максимальному резервированию баланса
    def _adjust_size_by_margin_limit(
        self,
        *,
        state: AutoTradeState,
        entry_price: float,
        size: float,
    ) -> float:
        max_percent = safe_float(state.max_reserved_balance_percent)

        if max_percent is None or max_percent <= 0:
            return self._round_size(size)

        leverage = safe_float(state.leverage) or 1.0
        price = safe_float(entry_price)
        current_size = safe_float(size) or 0.0

        if leverage <= 0 or price is None or price <= 0:
            state.execution_block_reason = "Invalid leverage or entry price."
            return 0.0

        balance_usd = safe_float(state.allocated_balance_usd) or 0.0
        max_reserved_usd = balance_usd * (max_percent / 100)
        max_notional_usd = max_reserved_usd * leverage
        max_size = max_notional_usd / price

        if current_size <= max_size:
            return self._round_size(current_size)

        state.execution_size_adjustment_reason = "MARGIN_LIMIT"

        limited_size = self._round_size(max_size)
        adaptive_final = safe_float(state.adaptive_size_final) or 0.0

        if adaptive_final > 0:
            effective_multiplier = limited_size / adaptive_final

            if effective_multiplier < 0.5:
                state.adaptive_size_reason = "размер позиции сильно ограничен margin limit"
            else:
                state.adaptive_size_reason = "размер позиции ограничен margin limit"

        return limited_size

    # округлить размер позиции вниз до допустимой точности
    def _round_size(self, size: NumericLike | None) -> float:
        value = safe_float(size)

        if value is None:
            return 0.0

        factor = 10 ** self._size_precision
        return math.floor(value * factor) / factor