# app/src/trading/execution/position_exit_decision.py
 
from __future__ import annotations

import time
from typing import ClassVar, Protocol

from src.core.numbers import safe_float
from src.trading.auto.state import AutoTradeState
from src.trading.execution.position_metrics import PositionMetrics, build_position_metrics
from src.trading.position.state import PositionState
from src.trading.execution.constants import get_position_exit_thresholds


class _ExecutionPositionExitDecisionProtocol(Protocol):
    _position: ClassVar[PositionState]


class ExecutionPositionExitDecisionMixin(_ExecutionPositionExitDecisionProtocol):
    """
    Execution-слой принятия решения о runtime-закрытии позиции.

    Важно:
    - этот файл НЕ рассчитывает PnL, движение цены и время удержания сам;
    - все числовые метрики позиции берутся из position_metrics.py;
    - здесь остаётся только логика принятия решения: закрывать позицию или нет.
    """

    def _runtime_intelligence_close_reason(
        self,
        *,
        state: AutoTradeState,
        current_price: float,
    ) -> str | None:
        metrics = build_position_metrics(
            type(self)._position,
            current_price=current_price,
        )

        # Защита от раннего выхода на обычной волне/откате.
        # Если позиция открыта недавно и просадка ещё в рамках нормальной
        # волатильности актива, intelligence-close не закрывает сделку.
        if self._is_normal_pullback_wave(state=state, metrics=metrics):
            return None

        giveback_reason = self._apply_intelligence_exit(
            state=state,
            reason=self._giveback_close_reason(
                state=state,
                metrics=metrics,
            ),
            algorithm="GIVEBACK",
        )

        if giveback_reason is not None:
            return giveback_reason

        time_decay_reason = self._apply_intelligence_exit(
            state=state,
            reason=self._time_decay_close_reason(
                state=state,
                metrics=metrics,
            ),
            algorithm="TIME_DECAY",
        )

        if time_decay_reason is not None:
            return time_decay_reason

        return None

    def _sync_intelligence_exit_state(
        self,
        *,
        state: AutoTradeState,
        reason: str,
        algorithm: str,
    ) -> None:
        # В AutoTradeState сейчас нет отдельного поля position_exit_algorithm.
        # Поэтому алгоритм пишем в position_intelligence_reason — это поле уже есть
        # в state и попадёт дальше в диагностику / журнал закрытия.
        state.position_intelligence_reason = algorithm

        state.runtime_protection_action = "INTELLIGENCE_EXIT"
        state.runtime_protection_reason = reason
        state.runtime_protection_updated_at = time.monotonic()

    def _apply_intelligence_exit(
        self,
        *,
        state: AutoTradeState,
        reason: str | None,
        algorithm: str,
    ) -> str | None:
        if reason is None:
            return None

        self._sync_intelligence_exit_state(
            state=state,
            reason=reason,
            algorithm=algorithm,
        )

        return reason
    
    def _giveback_close_reason(
        self,
        *,
        state: AutoTradeState,
        metrics: PositionMetrics,
    ) -> str | None:
        price_move_percent = metrics.price_move_percent

        peak_percent = safe_float(
            getattr(state, "position_peak_pnl_percent", None)
        )

        if peak_percent is None or peak_percent <= 0:
            return None

        giveback = peak_percent - price_move_percent

        if giveback <= 0:
            return None

        giveback_percent = round((giveback / peak_percent) * 100, 2)

        # Сохраняем рассчитанный giveback в state,
        # чтобы журнал закрытия видел именно то значение,
        # на основании которого принято решение.
        state.position_giveback_percent = giveback_percent

        fatigue_state = str(
            getattr(state, "position_fatigue_state", "") or ""
        ).upper()

        reversal_risk = str(
            getattr(state, "position_reversal_risk", "") or ""
        ).upper()

        adverse_momentum = bool(
            getattr(state, "position_adverse_momentum", False)
        )

        exit_confidence = safe_float(
            getattr(state, "position_exit_confidence", None)
        ) or 0.0

        thresholds = self._exit_thresholds(state)

        market_quality = str(
            getattr(state, "market_trend_quality", "") or ""
        ).upper()

        stall_state = str(
            getattr(state, "position_stall_state", "") or ""
        ).upper()

        # В CLEAN рынке даём прибыли больше пространства.
        # В NOISY рынке фиксируем быстрее, потому что откаты чаще съедают прибыль.
        if market_quality == "NOISY":
            min_peak = thresholds["noisy_giveback_min_peak"]
            giveback_limit = thresholds["noisy_giveback_percent"]
        else:
            min_peak = thresholds["clean_giveback_min_peak"]
            giveback_limit = thresholds["clean_giveback_percent"]

        if (
            peak_percent >= min_peak
            and giveback_percent >= giveback_limit
            and price_move_percent > 0.10
        ):
            return (
                "NOISY_GIVEBACK_EXIT"
                if market_quality == "NOISY"
                else "CLEAN_GIVEBACK_EXIT"
            )

        if (
            stall_state in {"NOISY_STALLED", "ADVERSE_STALLED"}
            and peak_percent >= min_peak
            and giveback_percent >= max(25, giveback_limit - 10)
            and price_move_percent > 0
        ):
            return "STALL_GIVEBACK_EXIT"

        if (
            peak_percent >= 1.50
            and giveback_percent >= 50
            and price_move_percent > 0.25
        ):
            return "GIVEBACK_PROFIT_LOCK"

        if (
            peak_percent >= 1.20
            and giveback_percent >= 60
            and price_move_percent > 0.15
        ):
            return "GIVEBACK_PROTECTION"

        if (
            peak_percent >= 1.00
            and giveback_percent >= 50
            and adverse_momentum
        ):
            return "GIVEBACK_MOMENTUM_REVERSAL"

        if (
            peak_percent >= 1.00
            and giveback_percent >= 45
            and fatigue_state in {"TIRED", "EXHAUSTED"}
        ):
            return "GIVEBACK_FATIGUE_EXIT"

        if (
            peak_percent >= 1.00
            and giveback_percent >= 45
            and reversal_risk in {"ELEVATED", "HIGH"}
            and exit_confidence >= 0.60
        ):
            return "GIVEBACK_REVERSAL_RISK"

        return None

    def _time_decay_close_reason(
        self,
        *,
        state: AutoTradeState,
        metrics: PositionMetrics,
    ) -> str | None:
        hold_seconds = metrics.hold_seconds

        if hold_seconds is None:
            return None

        price_move_percent = metrics.price_move_percent
        thresholds = self._exit_thresholds(state)

        # Hard-loss — отдельный аварийный intelligence-exit.
        # Если движение цены уже глубже допустимого порога,
        # не ждём fatigue / time-decay / adverse momentum.
        if price_move_percent <= thresholds["hard_loss"]:
            return "HARD_LOSS_EXIT"

        fatigue_state = str(
            getattr(state, "position_fatigue_state", "") or ""
        ).upper()

        conviction_state = str(
            getattr(state, "position_conviction_state", "") or ""
        ).upper()

        decay_state = str(
            getattr(state, "position_decay_state", "") or ""
        ).upper()

        adverse_momentum = bool(
            getattr(state, "position_adverse_momentum", False)
        )

        market_runtime_degraded = bool(
            getattr(state, "market_runtime_degraded", False)
        )

        net_pnl_usd = safe_float(getattr(metrics, "net_pnl_usd", None)) or 0.0

        risk_level = str(
            getattr(state, "position_risk_level", "") or ""
        ).upper()

        # Time-decay не должен закрывать позицию просто потому,
        # что она долго стоит около нуля.
        # Разрешаем time-decay закрытие только если:
        # - сделка уже покрыла RT-комиссию и net PnL положительный;
        # - или есть реальное ухудшение: adverse momentum / HIGH risk / BROKEN conviction.
        real_deterioration = (
            adverse_momentum
            or risk_level == "HIGH"
            or conviction_state == "BROKEN"
        )

        market_quality = str(
            getattr(state, "market_trend_quality", "") or ""
        ).upper()

        peak_percent = safe_float(
            getattr(state, "position_peak_pnl_percent", None)
        ) or 0.0

        giveback_percent = safe_float(
            getattr(state, "position_giveback_percent", None)
        ) or 0.0

        # Специальный быстрый выход для NOISY рынка.
        # В шумном рынке не ждём классический time-decay 1500-2100 секунд:
        # если позиция после минимального времени уже в минусе
        # или быстро отдаёт прибыль, закрываем раньше.
        if market_quality == "NOISY" and hold_seconds >= thresholds["noisy_min_hold"]:
            if (
                price_move_percent <= thresholds["noisy_loss_exit"]
                and adverse_momentum
            ):
                return "NOISY_ADVERSE_EXIT"

            if (
                peak_percent > 0
                and giveback_percent >= thresholds["noisy_profit_giveback"]
                and price_move_percent > 0
            ):
                return "NOISY_PROFIT_GIVEBACK_EXIT"

        if net_pnl_usd <= 0 and not real_deterioration:
            return None

        # Нейтральную позицию по ETH/BTC/LTC/XRP держим дольше.
        # Например для ETH: если движение внутри ±0.40%,
        # не закрываем её по time-decay раньше neutral_min_hold.
        if (
            hold_seconds < thresholds["neutral_min_hold"]
            and abs(price_move_percent) <= thresholds["neutral_band"]
            and not real_deterioration
        ):
            return None

        if (
            hold_seconds >= thresholds["neutral_min_hold"]
            and -thresholds["neutral_band"] <= price_move_percent <= thresholds["neutral_band"]
            and conviction_state in {"WEAKENING", "BROKEN", "NEUTRAL"}
        ):
            return "TIME_DECAY_EXIT"

        if (
            hold_seconds >= thresholds["min_hold"]
            and -thresholds["neutral_band"] <= price_move_percent <= thresholds["neutral_band"]
            and fatigue_state in {"TIRED", "EXHAUSTED"}
        ):
            return "TIME_DECAY_FATIGUE_EXIT"

        if (
            hold_seconds >= thresholds["min_hold"]
            and price_move_percent <= thresholds["normal_pullback"]
            and adverse_momentum
        ):
            return "TIME_DECAY_ADVERSE_MOMENTUM"

        if (
            hold_seconds >= thresholds["min_hold"]
            and price_move_percent <= thresholds["normal_pullback"]
            and market_runtime_degraded
        ):
            return "TIME_DECAY_DEGRADED_MARKET"

        if (
            hold_seconds >= thresholds["neutral_min_hold"]
            and decay_state in {"TIME_DECAY", "CONTEXT_DECAY"}
            and price_move_percent <= thresholds["neutral_band"]
        ):
            return "TIME_DECAY_CONTEXT_DECAY"

        return None

    def _exit_thresholds(self, state: AutoTradeState) -> dict[str, float]:
        return get_position_exit_thresholds(
            getattr(state, "symbol", None)
        )

    def _is_normal_pullback_wave(
        self,
        *,
        state: AutoTradeState,
        metrics: PositionMetrics,
    ) -> bool:
        thresholds = self._exit_thresholds(state)

        hold_seconds = safe_float(metrics.hold_seconds)
        price_move_percent = safe_float(metrics.price_move_percent)

        if hold_seconds is None or price_move_percent is None:
            return False

        # Если убыток уже глубже hard_loss — это не обычный откат.
        if price_move_percent <= thresholds["hard_loss"]:
            return False

        if hold_seconds >= thresholds["min_hold"]:
            return False

        if price_move_percent < thresholds["normal_pullback"]:
            return False

        adverse_momentum = bool(
            getattr(state, "position_adverse_momentum", False)
        )

        risk_level = str(
            getattr(state, "position_risk_level", "") or ""
        ).upper()

        conviction_state = str(
            getattr(state, "position_conviction_state", "") or ""
        ).upper()

        # Если есть реальное ухудшение, это уже не обычный откат.
        # Так мы не блокируем быстрый выход в NOISY рынке,
        # когда momentum/риск явно против позиции.
        if adverse_momentum or risk_level == "HIGH" or conviction_state == "BROKEN":
            return False

        market_phase = str(getattr(state, "market_phase", "") or "").upper()
        market_quality = str(getattr(state, "market_trend_quality", "") or "").upper()
        market_structure = str(getattr(state, "market_structure", "") or "").upper()
        trend_alignment = str(getattr(state, "position_trend_alignment", "") or "").upper()

        # Обычный откат/шум/флэт после входа не должен сразу закрывать сделку.
        if market_phase in {"PULLBACK", "RANGE", "SQUEEZE"}:
            return True

        if market_quality == "NOISY" and trend_alignment != "AGAINST":
            return True

        if market_structure in {"HH_HL", "LH_LL", "MIXED"} and trend_alignment != "AGAINST":
            return True

        return False