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

from __future__ import annotations

import time
from typing import Protocol

from src.core.event_bus import EventBus
from src.core.numbers import safe_float
from src.core.types import JsonDict
from src.trading.auto.state import AutoTradeState
from src.trading.execution.models import ExecutionDecision
from src.trading.execution.payloads import (
    build_decision_payload,
    build_execution_price_payload,
    build_execution_quality_payload,
    build_market_context_payload,
    build_risk_settings_payload,
    build_runtime_blocks_payload,
    build_runtime_payload,
    build_signal_payload,
)
from src.trading.journal.service import JournalService


class _ExecutionSupervisorProtocol(Protocol):
    _emergency_halt_drawdown_usd: float
    _emergency_halt_loss_streak: int
    _execution_cooldown_after_loss_seconds: int
    _max_execution_snapshot_age_seconds: int
    _degraded_market_block_states: set[str]
    _conflict_execution_block: bool
    _last_supervisor_block_key: str | None


class ExecutionSupervisorMixin(_ExecutionSupervisorProtocol):
    def _process_execution_supervisor(
        self,
        state: AutoTradeState,
    ) -> ExecutionDecision | None:
        for reason, action in (
            (self._execution_halt_reason(state), "EXECUTION_HALTED"),
            (self._execution_cooldown_reason(state), "EXECUTION_COOLDOWN"),
            (self._degraded_market_reason(state), "DEGRADED_MARKET"),
            (self._stale_execution_reason(state), "STALE_EXECUTION"),
            (self._entry_block_reason(state), "ENTRY_BLOCKED"),
            (self._low_execution_confidence_reason(state), "LOW_EXECUTION_CONFIDENCE"),
            (self._conflict_signal_reason(state), "SIGNAL_CONFLICT"),
        ):
            if reason is not None:
                return self._block_execution(
                    state=state,
                    reason=reason,
                    action=action,
                )

        self._clear_supervisor_block_state(state)
        return None

    def _clear_supervisor_block_state(
        self,
        state: AutoTradeState,
    ) -> None:
        supervisor_actions = {
            "EXECUTION_HALTED",
            "EXECUTION_COOLDOWN",
            "DEGRADED_MARKET",
            "STALE_EXECUTION",
            "ENTRY_BLOCKED",
            "LOW_EXECUTION_CONFIDENCE",
            "SIGNAL_CONFLICT",
        }

        state.execution_block_title = None
        state.execution_block_message = None
        state.execution_block_action = None

        if state.last_execution_action not in supervisor_actions:
            return

        if state.execution_block_reason == state.last_execution_reason:
            state.execution_block_reason = None

        type(self)._last_supervisor_block_key = None

    def _execution_halt_reason(self, state: AutoTradeState) -> str | None:
        pnl = safe_float(state.cycle_realized_pnl_usd) or 0.0

        if pnl <= -abs(self._emergency_halt_drawdown_usd):
            return "execution emergency halt: cycle drawdown limit exceeded"

        # Блокируем цикл только после серии подряд идущих убытков.
        # Важно: не считаем все убыточные сделки цикла, потому что прибыльная сделка
        # должна сбрасывать серию убытков.
        losses = int(getattr(state, "cycle_consecutive_losses", 0) or 0)

        if losses >= self._emergency_halt_loss_streak:
            return "execution emergency halt: loss streak exceeded"

        return None

    def _execution_cooldown_reason(self, state: AutoTradeState) -> str | None:
        # Cooldown после убытка включаем только если его явно активировал execution layer.
        # Сам факт last_loss_monotonic_at больше НЕ должен блокировать торговлю,
        # иначе пауза появляется уже после первой убыточной сделки.
        if not bool(getattr(state, "loss_cooldown_active", False)):
            return None

        ts = safe_float(getattr(state, "last_loss_monotonic_at", None))

        if ts is None:
            return None

        delta = time.monotonic() - ts

        if delta < self._execution_cooldown_after_loss_seconds:
            remaining = int(self._execution_cooldown_after_loss_seconds - delta)
            return f"execution cooldown after loss ({remaining}s remaining)"

        return None

    # разрешить ранний вход из RANGE, если уже есть impulse/momentum по сигналу
    def _early_impulse_execution_allowed(self, state: AutoTradeState) -> bool:
        signal = str(getattr(state, "last_signal", "") or "").upper()
        market_state = str(getattr(state, "market_state", "") or "").upper()
        market_phase = str(getattr(state, "market_phase", "") or "").upper()
        market_trend = str(getattr(state, "market_trend", "") or "").upper()
        momentum_state = str(getattr(state, "momentum_state", "") or "").upper()
        momentum_direction = str(getattr(state, "momentum_direction", "") or "").upper()
        htf_alignment = str(getattr(state, "htf_alignment", "") or "").upper()

        if signal not in {"BUY", "SELL"}:
            return False

        if market_state != "RANGE":
            return False

        if market_phase != "IMPULSE":
            return False

        if htf_alignment not in {"ALIGNED", "SAME_INTERVAL"}:
            return False

        if signal == "BUY":
            return (
                market_trend == "UP"
                and momentum_direction == "UP"
                and momentum_state in {"MOMENTUM_UP", "BREAKOUT_UP"}
            )

        if signal == "SELL":
            return (
                market_trend == "DOWN"
                and momentum_direction == "DOWN"
                and momentum_state in {"MOMENTUM_DOWN", "BREAKOUT_DOWN"}
            )

        return False
    
    def _degraded_market_reason(self, state: AutoTradeState) -> str | None:
        market_state = str(getattr(state, "market_state", "") or "").upper()
        volatility = str(getattr(state, "market_volatility", "") or "").upper()

        early_impulse_allowed = self._early_impulse_execution_allowed(state)

        if market_state in self._degraded_market_block_states:
            if market_state == "RANGE" and early_impulse_allowed:
                return None

            return f"market state blocked execution: {market_state}"

        if market_state in {"RANGE", "LOW_VOLATILITY", "UNKNOWN", ""}:
            if market_state == "RANGE" and early_impulse_allowed:
                return None

            return f"market state blocked execution: {market_state or 'UNKNOWN'}"

        if volatility in {"LOW", "UNKNOWN", ""}:
            return f"market volatility blocked execution: {volatility or 'UNKNOWN'}"

        return None

    def _stale_execution_reason(self, state: AutoTradeState) -> str | None:
        age = safe_float(getattr(state, "execution_price_age_seconds", None))

        if age is None:
            age = safe_float(getattr(state, "snapshot_age_seconds", None))

        if age is None:
            return None

        if age > self._max_execution_snapshot_age_seconds:
            return f"execution snapshot stale: {age:.2f}s"

        return None

    def _entry_block_reason(self, state: AutoTradeState) -> str | None:
        reason = str(getattr(state, "entry_block_reason", "") or "").strip()
        message = str(getattr(state, "entry_block_message", "") or "").strip()

        if reason:
            return message or f"entry blocked by market analysis: {reason}"

        return None

    def _low_execution_confidence_reason(self, state: AutoTradeState) -> str | None:
        signal = str(getattr(state, "last_signal", "") or "").upper()

        if signal not in {"BUY", "SELL"}:
            return None

        score = safe_float(getattr(state, "execution_confidence_score", None))
        required = safe_float(
            getattr(state, "execution_confidence_required_score", None)
        )

        if required is None:
            required = 0.55

        if score is None:
            return "execution confidence is not calculated"

        if score < required:
            return f"execution confidence too low: {score:.2f} < {required:.2f}"

        return None

    def _conflict_signal_reason(self, state: AutoTradeState) -> str | None:
        if not self._conflict_execution_block:
            return None

        signal = str(getattr(state, "last_signal", "") or "").upper()
        momentum_direction = str(getattr(state, "momentum_direction", "") or "").upper()
        trend_direction = str(getattr(state, "market_trend", "") or "").upper()
        market_state = str(getattr(state, "market_state", "") or "").upper()
        market_structure = str(getattr(state, "market_structure", "") or "").upper()
        htf_alignment = str(getattr(state, "htf_alignment", "") or "").upper()

        if signal not in {"BUY", "SELL"}:
            return None

        if htf_alignment and htf_alignment not in {"ALIGNED", "SAME_INTERVAL"}:
            return f"{signal} conflicts with HTF alignment: {htf_alignment}"

        if signal == "BUY":
            if momentum_direction == "DOWN":
                return "BUY conflicts with momentum"

            if trend_direction == "DOWN" or market_state == "TREND_DOWN":
                return "BUY conflicts with trend"

            if market_structure == "LH_LL":
                return "BUY conflicts with bearish market structure"

        if signal == "SELL":
            if momentum_direction == "UP":
                return "SELL conflicts with momentum"

            if trend_direction == "UP" or market_state == "TREND_UP":
                return "SELL conflicts with trend"

            if market_structure == "HH_HL":
                return "SELL conflicts with bullish market structure"

        return None

    def _human_execution_block(
        self,
        *,
        action: str,
        reason: str,
        state: AutoTradeState,
    ) -> tuple[str, str, str]:
        if action == "EXECUTION_HALTED":
            # Для UI показываем именно текущую серию убытков подряд,
            # а не общее количество минусовых сделок за цикл.
            losses = int(getattr(state, "cycle_consecutive_losses", 0) or 0)

            if "loss streak" in reason:
                return (
                    "Совершение сделок заблокировано",
                    f"Превышен лимит убыточных сделок · {losses}",
                    "Перезапусти цикл автоторговли",
                )

            return (
                "Совершение сделок заблокировано",
                "Превышен лимит просадки цикла",
                "Перезапусти цикл автоторговли",
            )

        if action == "EXECUTION_COOLDOWN":
            return (
                "Совершение сделок временно заблокировано",
                "Пауза после убыточной сделки",
                "Дождись окончания cooldown",
            )

        if action == "LOW_EXECUTION_CONFIDENCE":
            return (
                "Сделка заблокирована",
                "Низкая уверенность исполнения",
                "Дождись более сильного сигнала",
            )

        if action == "ENTRY_BLOCKED":
            return (
                "Сделка заблокирована",
                str(getattr(state, "entry_block_message", "") or "Рынок сейчас не подходит для входа"),
                "Дождись подходящих условий",
            )

        if action == "SIGNAL_CONFLICT":
            return (
                "Сделка заблокирована",
                "Сигнал конфликтует с рынком",
                "Дождись подтверждения направления",
            )

        if action == "STALE_EXECUTION":
            return (
                "Сделка заблокирована",
                "Нет актуальных котировок",
                "Дождись обновления данных",
            )

        if action == "DEGRADED_MARKET":
            return (
                "Сделка заблокирована",
                "Рыночные условия не подходят",
                "Дождись нормализации рынка",
            )

        return (
            "Сделка заблокирована",
            reason,
            "Проверь журнал автоторговли",
        )

    def _build_supervisor_block_payload(
        self,
        *,
        state: AutoTradeState,
        action: str,
        reason: str,
    ) -> JsonDict:
        return {
            # ---------- Event ----------
            "execution_type": "SUPERVISOR_BLOCK",
            "action": action,

            # В supervisor reason — причина блокировки.
            # Поэтому signal reason сохраняем отдельно ниже как signal_reason.
            "reason": reason,

            # ---------- Instrument ----------
            "symbol": state.symbol,

            **build_runtime_payload(state),

            # build_signal_payload даёт поле reason как причину сигнала,
            # поэтому ниже reason события перезаписываем обратно.
            **build_signal_payload(state),
            "signal_reason": state.last_signal_reason,
            "reason": reason,

            **build_decision_payload(state),
            "is_signal_confirmed": state.is_signal_confirmed,
            "is_signal_ready": state.is_signal_ready,

            **build_runtime_blocks_payload(state),
            **build_execution_quality_payload(state),

            "execution_confidence_required_score": (
                state.execution_confidence_required_score
            ),
            "execution_confidence_factors": state.execution_confidence_factors,

            **build_execution_price_payload(state),
            **build_risk_settings_payload(state),

            # ---------- Position ----------
            "position_side": state.position_side,
            "entry_price": state.entry_price,
            "position_size": state.position_size,
            "unrealized_pnl_usd": state.unrealized_pnl_usd,

            # ---------- Cycle stats ----------
            "realized_pnl_usd": state.realized_pnl_usd,
            "cycle_realized_pnl_usd": state.cycle_realized_pnl_usd,
            "cycle_closed_trades": state.cycle_closed_trades,
            "cycle_winning_trades": state.cycle_winning_trades,
            "cycle_losing_trades": state.cycle_losing_trades,
            "cycle_consecutive_losses": state.cycle_consecutive_losses,
            "loss_cooldown_active": state.loss_cooldown_active,
            "loss_cooldown_reason": state.loss_cooldown_reason,

            **build_market_context_payload(state),
        }
    
    def _block_execution(
        self,
        *,
        state: AutoTradeState,
        reason: str,
        action: str,
    ) -> ExecutionDecision:
        state.execution_block_reason = reason
        state.last_execution_action = action
        state.last_execution_reason = reason

        (
            state.execution_block_title,
            state.execution_block_message,
            state.execution_block_action,
        ) = self._human_execution_block(
            action=action,
            reason=reason,
            state=state,
        )

        key_reason = reason

        if action == "EXECUTION_COOLDOWN":
            key_reason = "execution cooldown after loss"

        key = f"{action}:{state.symbol}:{key_reason}"
        last_key = getattr(type(self), "_last_supervisor_block_key", None)

        if key != last_key:
            setattr(type(self), "_last_supervisor_block_key", key)

            payload = self._build_supervisor_block_payload(
                state=state,
                action=action,
                reason=reason,
            )

            JournalService().log_ui_warning(
                event_type="execution_supervisor_block",
                message=f"Execution supervisor blocked action: {reason}",
                screen="auto",
                action="execution_supervisor",
                payload=payload,
            )

            EventBus.emit("execution_supervisor_block", payload)

        return ExecutionDecision("NONE", False, reason)