# app/src/trading/execution/position_actions.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, NumericLike
from src.trading.auto.state import AutoTradeState
from src.trading.auto.state_reset import (
    reset_autonomous_runtime_state,
    reset_execution_block_state,
)
from src.trading.execution.models import ExecutionDecision
from src.trading.execution.payloads import (
    build_adaptive_size_payload,
    build_decision_payload,
    build_execution_price_payload,
    build_execution_quality_payload,
    build_market_context_payload,
    build_position_health_payload,
    build_position_intelligence_payload,
    build_risk_settings_payload,
    build_runtime_blocks_payload,
    build_runtime_payload,
    build_signal_payload,
    build_autonomous_payload,
)
from src.trading.execution.pricing import ExecutionPrice
from src.trading.journal.service import JournalService
from src.trading.position.state import PositionState
from src.trading.execution.position_metrics import build_position_metrics
from src.trading.execution.constants import (
    EXECUTION_ACTION_CLOSE,
    EXECUTION_ACTION_FORCE_CLOSE_PREFIX,
    EXECUTION_ACTION_NONE,
    EXECUTION_MAX_CONSECUTIVE_LOSSES,
    EXECUTION_REASON_MANUAL,
    EXECUTION_TYPE_ENTRY,
    EXECUTION_TYPE_ENTRY_REJECTED,
    EXECUTION_TYPE_EXIT,
    POSITION_SIDE_NONE,
    PRICING_ENTRY_MODE,
    PRICING_EXIT_MODE,
)


class _ExecutionPositionActionsProtocol(Protocol):
    _position: PositionState
    _last_flip_block_key: str | None

    def _create_trade_id(
        self,
        state: AutoTradeState,
        side: str,
    ) -> str: ...

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

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

    def _calculate_position_size(
        self,
        state: AutoTradeState,
        *,
        entry_price: float | None = None,
    ) -> float: ...

    def _adjust_size_by_margin_limit(
        self,
        *,
        state: AutoTradeState,
        entry_price: float,
        size: float,
    ) -> float: ...

    def _sync_effective_risk_after_margin_limit(
        self,
        state: AutoTradeState,
        *,
        base_size: float,
        final_size: float,
    ) -> None: ...

    def _round_size(self, size: NumericLike | None) -> float: ...

    def _sync_state_from_position(
        self,
        state: AutoTradeState,
    ) -> None: ...

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

    def _reset_runtime_protection_state(
        self,
        state: AutoTradeState,
    ) -> None: ...

    def _reset_position_lifecycle_state(
        self,
        state: AutoTradeState,
    ) -> None: ...


class ExecutionPositionActionsMixin(_ExecutionPositionActionsProtocol):
    # ---------- Trade id ----------
    # создать новый trade_id для связки open -> close
    def _create_trade_id(self, state: AutoTradeState, side: str) -> str:
        state.trade_sequence = int(state.trade_sequence or 0) + 1
        cycle_number = int(state.cycle_number or 0)

        return (
            f"trade-{cycle_number}-"
            f"{state.trade_sequence}-"
            f"{side.lower()}-"
            f"{int(time.time())}"
        )

    # ---------- Payload builders ----------
    # собрать payload отказа открытия позиции без изменения состояния
    def _build_position_open_rejected_payload(
        self,
        *,
        state: AutoTradeState,
        side: str,
        action: str,
        reason: str,
    ) -> JsonDict:
        return {
            # ---------- Event ----------
            "execution_type": EXECUTION_TYPE_ENTRY_REJECTED,
            "action": action,
            "reject_reason": reason,

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

            **build_runtime_payload(state),
            **build_signal_payload(state),
            **build_decision_payload(state),
            **build_runtime_blocks_payload(state),
            **build_execution_quality_payload(state),
            **build_execution_price_payload(state),
            **build_adaptive_size_payload(state),
            **build_risk_settings_payload(state),
            **build_market_context_payload(state),
        }

    # собрать payload успешного открытия позиции без изменения состояния
    def _build_position_opened_payload(
        self,
        *,
        state: AutoTradeState,
        side: str,
        action: str,
        trade_id: str,
        entry_price: float,
        size: float,
        now: str,
        opened_monotonic_at: float,
        entry: ExecutionPrice,
    ) -> JsonDict:
        return {
            # ---------- Trade ----------
            "trade_id": trade_id,
            "trade_sequence": state.trade_sequence,
            "trade_cycle_number": state.current_trade_cycle_number,

            "execution_type": EXECUTION_TYPE_ENTRY,
            "action": action,

            # ---------- Position ----------
            "symbol": state.symbol,
            "side": side,
            "entry_price": entry_price,
            "size": size,

            "opened_at": now,
            "opened_monotonic_at": opened_monotonic_at,

            # ---------- Pricing ----------
            "pricing": PRICING_ENTRY_MODE,
            "pricing_role": entry.pricing_role,
            "price_source": entry.source,
            "price_age_seconds": entry.age_seconds,
            "price_updated_at": entry.updated_at,

            **build_runtime_payload(state),
            **build_position_health_payload(state),
            **build_signal_payload(state),
            **build_decision_payload(state),
            **build_runtime_blocks_payload(state),
            **build_execution_quality_payload(state),
            **build_execution_price_payload(state),
            **build_adaptive_size_payload(state),
            **build_risk_settings_payload(state),
            **build_market_context_payload(state),
        }

    # собрать payload закрытия позиции без изменения состояния
    def _build_position_closed_payload(
        self,
        *,
        state: AutoTradeState,
        position: PositionState,
        trade_id: str | None,
        exit_price: float,
        exit_execution: ExecutionPrice | None,
        metrics,
        pnl: float,
        price_move_percent: float | None,
        close_reason: str,
        forced_reason: str | None,
        now: str,
    ) -> JsonDict:
        return {
            # ---------- Trade ----------
            "trade_id": trade_id,
            "trade_sequence": position.trade_sequence or state.trade_sequence,
            "trade_cycle_number": (
                position.trade_cycle_number
                or state.current_trade_cycle_number
            ),

            "execution_type": EXECUTION_TYPE_EXIT,
            "action": EXECUTION_ACTION_CLOSE,
            "risk_reason": forced_reason,
            "close_reason": close_reason,
            "is_forced": forced_reason is not None,

            **build_runtime_payload(state),
            **build_signal_payload(state),
            **build_decision_payload(state),
            **build_runtime_blocks_payload(state),
            **build_execution_quality_payload(state),
            **build_execution_price_payload(state),
            **build_adaptive_size_payload(state),
            **build_risk_settings_payload(state),

            # ---------- Instrument / Position ----------
            "symbol": state.symbol,
            "side": position.side,
            "entry_price": position.entry_price,
            "exit_price": exit_price,
            "size": position.size,
            "leverage": position.leverage,

            "opened_at": position.opened_at,
            "closed_at": now,

            # ---------- PnL / Metrics ----------
            "pnl": pnl,
            "net_pnl_usd": metrics.net_pnl_usd,
            "gross_pnl_usd": metrics.gross_pnl_usd,
            "commission_usd": metrics.commission_usd,
            "overnight_cashflow_usd": metrics.overnight_cashflow_usd,
            "pnl_percent": metrics.pnl_percent,
            "price_move_percent": price_move_percent,

            "entry_notional_usd": metrics.entry_notional_usd,
            "current_notional_usd": metrics.current_notional_usd,
            "margin_usd": metrics.margin_usd,
            "hold_seconds": metrics.hold_seconds,
            "overnight_count": metrics.overnight_count,

            # ---------- Pricing ----------
            "pricing": PRICING_EXIT_MODE,
            "pricing_role": exit_execution.pricing_role if exit_execution else None,
            "price_source": exit_execution.source if exit_execution else None,
            "price_age_seconds": exit_execution.age_seconds if exit_execution else None,
            "price_updated_at": exit_execution.updated_at if exit_execution else None,

            # ---------- Cycle Stats Before Close Sync ----------
            "realized_pnl_usd_before": state.realized_pnl_usd,
            "cycle_realized_pnl_usd_before": state.cycle_realized_pnl_usd,
            "cycle_closed_trades_before": state.cycle_closed_trades,
            "cycle_winning_trades_before": state.cycle_winning_trades,
            "cycle_losing_trades_before": state.cycle_losing_trades,
            "cycle_consecutive_losses_before": state.cycle_consecutive_losses,
            "cycle_trade_fees_usd_before": state.cycle_trade_fees_usd,
            "cycle_overnight_fees_usd_before": state.cycle_overnight_fees_usd,

            **build_position_health_payload(state),
            **build_position_intelligence_payload(state),
            **build_autonomous_payload(state),
            **build_market_context_payload(state),
        }

    # ---------- Journal helpers ----------
    # записать отказ открытия позиции в журнал
    def _log_position_open_rejected(
        self,
        *,
        state: AutoTradeState,
        side: str,
        action: str,
        reason: str,
    ) -> None:
        payload = self._build_position_open_rejected_payload(
            state=state,
            side=side,
            action=action,
            reason=reason,
        )

        JournalService().log_ui_warning(
            event_type="position_open_rejected",
            message=f"Открытие позиции {side} отклонено: {reason}",
            screen="auto",
            action="paper_execution",
            payload=payload,
        )

    # ---------- Decision helpers ----------
    # записать отказ открытия позиции и вернуть стандартное решение без исполнения
    def _reject_position_open(
        self,
        *,
        state: AutoTradeState,
        side: str,
        action: str,
        reason: str,
    ) -> ExecutionDecision:
        self._log_position_open_rejected(
            state=state,
            side=side,
            action=action,
            reason=reason,
        )

        return ExecutionDecision(EXECUTION_ACTION_NONE, False, reason)

    # ---------- Position actions ----------
    # открыть позицию, если сейчас позиции нет
    def _open_position_if_empty(
        self,
        *,
        state: AutoTradeState,
        side: str,
        action: str,
    ) -> ExecutionDecision:
        position = type(self)._position

        if position.side != POSITION_SIDE_NONE:
            self._sync_state_from_position(state)

            if position.side == side:
                reason = f"Позиция {side} уже открыта."
                return ExecutionDecision(EXECUTION_ACTION_NONE, False, reason)

            reason = (
                f"Позиция уже открыта в другом направлении: "
                f"{position.side}, новый запрос: {side}."
            )

            return self._reject_position_open(
                state=state,
                side=side,
                action=action,
                reason=reason,
            )

        try:
            entry = self._entry_price_for_side(state.symbol, side)
            entry_price = safe_float(entry.price)

        except Exception as exc:
            reason = f"Не удалось получить цену для paper execution: {exc}"

            return self._reject_position_open(
                state=state,
                side=side,
                action=action,
                reason=reason,
            )

        if entry_price is None or entry_price <= 0:
            reason = "Позиция не открыта: некорректная цена входа."

            return self._reject_position_open(
                state=state,
                side=side,
                action=action,
                reason=reason,
            )

        now = self._now_time()
        opened_monotonic_at = time.monotonic()

        # Перед новой позицией очищаем runtime-защиту и lifecycle,
        # чтобы новая сделка не унаследовала состояние прошлой позиции.
        self._reset_runtime_protection_state(state)
        self._reset_position_lifecycle_state(state)

        size = self._calculate_position_size(
            state,
            entry_price=entry_price,
        )

        if size <= 0:
            reason = "Позиция не открыта: невозможно рассчитать adaptive size."

            return self._reject_position_open(
                state=state,
                side=side,
                action=action,
                reason=reason,
            )

        base_size = size

        size = self._adjust_size_by_margin_limit(
            state=state,
            entry_price=entry_price,
            size=size,
        )

        size = self._round_size(size)

        self._sync_effective_risk_after_margin_limit(
            state,
            base_size=base_size,
            final_size=size,
        )

        if size <= 0:
            reason = "Позиция не открыта: итоговый size равен 0."

            return self._reject_position_open(
                state=state,
                side=side,
                action=action,
                reason=reason,
            )

        trade_id = self._create_trade_id(state, side)
        state.current_trade_id = trade_id
        state.current_trade_cycle_number = state.cycle_number

        type(self)._position = PositionState(
            trade_id=trade_id,
            trade_cycle_number=state.current_trade_cycle_number,
            trade_sequence=state.trade_sequence,
            side=side,
            symbol=state.symbol,
            entry_price=entry_price,
            size=size,
            leverage=state.leverage,
            unrealized_pnl_usd=0.0,
            opened_at=now,
            opened_monotonic_at=opened_monotonic_at,
            updated_at=now,
        )

        self._sync_state_from_position(state)

        # Сразу фиксируем monotonic-время открытия в AutoTradeState,
        # чтобы UI/protection/semantics не ждали следующего цикла.
        state.position_opened_monotonic_at = opened_monotonic_at

        reset_execution_block_state(state)
        state.last_flip_block_reason = None
        state.last_execution_action = action
        state.last_execution_reason = f"Позиция {side} открыта."

        payload = self._build_position_opened_payload(
            state=state,
            side=side,
            action=action,
            trade_id=trade_id,
            entry_price=entry_price,
            size=size,
            now=now,
            opened_monotonic_at=opened_monotonic_at,
            entry=entry,
        )

        JournalService().log_ui_info(
            event_type="position_opened",
            message=f"Позиция {side} открыта: {state.symbol}.",
            screen="auto",
            action="paper_execution",
            payload=payload,
        )

        EventBus.emit("paper_position_opened", payload)

        return ExecutionDecision(action, True, f"Позиция {side} открыта.")

    # закрыть открытую позицию
    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:
        position = type(self)._position

        if position.side == POSITION_SIDE_NONE:
            self._sync_state_from_position(state)

            return ExecutionDecision(
                EXECUTION_ACTION_NONE,
                False,
                "Нет открытой позиции для закрытия.",
            )

        if forced_exit_price is not None:
            exit_price = safe_float(forced_exit_price)
            exit_execution = forced_price_meta

        else:
            try:
                exit_execution = self._exit_price_for_side(
                    position.symbol or state.symbol,
                    position.side,
                )

                exit_price = safe_float(exit_execution.price)

            except Exception as exc:
                return ExecutionDecision(
                    EXECUTION_ACTION_NONE,
                    False,
                    f"Ошибка получения цены для закрытия: {exc}",
                )

        if exit_price is None or exit_price <= 0:
            return ExecutionDecision(
                EXECUTION_ACTION_NONE,
                False,
                "Ошибка закрытия позиции: некорректная цена выхода.",
            )

        metrics = build_position_metrics(
            position,
            current_price=exit_price,
        )

        pnl = (
            safe_float(forced_pnl)
            if forced_pnl is not None
            else metrics.net_pnl_usd
        )

        if pnl is None:
            pnl = 0.0

        price_move_percent = metrics.price_move_percent
        close_reason = forced_reason or EXECUTION_REASON_MANUAL
        now = self._now_time()

        trade_id = position.trade_id or state.current_trade_id

        payload = self._build_position_closed_payload(
            state=state,
            position=position,
            trade_id=trade_id,
            exit_price=exit_price,
            exit_execution=exit_execution,
            metrics=metrics,
            pnl=pnl,
            price_move_percent=price_move_percent,
            close_reason=close_reason,
            forced_reason=forced_reason,
            now=now,
        )

        state.realized_pnl_usd += pnl
        state.cycle_realized_pnl_usd += pnl
        state.cycle_closed_trades += 1

        # Комиссии закрытой сделки добавляем один раз.
        # Важно: выше по функции этих начислений быть не должно, иначе UI покажет x2.
        state.cycle_trade_fees_usd += abs(safe_float(metrics.commission_usd) or 0.0)
        state.cycle_overnight_fees_usd += safe_float(metrics.overnight_cashflow_usd) or 0.0

        if pnl > 0:
            state.cycle_winning_trades += 1

            # Прибыльная сделка сбрасывает серию подряд идущих убытков.
            state.cycle_consecutive_losses = 0
            state.loss_cooldown_active = False
            state.loss_cooldown_reason = None

        elif pnl < 0:
            state.cycle_losing_trades += 1
            state.cycle_consecutive_losses += 1
            state.last_loss_monotonic_at = time.monotonic()

            # Не включаем cooldown после первого убытка.
            # Supervisor остановит торговлю только когда серия достигнет лимита.
            if state.cycle_consecutive_losses >= EXECUTION_MAX_CONSECUTIVE_LOSSES:
                state.loss_cooldown_active = True
                state.loss_cooldown_reason = (
                    f"{state.cycle_consecutive_losses} подряд убыточных сделок"
                )

        JournalService().log_ui_info(
            event_type="position_closed",
            message=f"Позиция {position.side} закрыта: {close_reason}.",
            screen="auto",
            action="paper_execution",
            payload=payload,
        )

        EventBus.emit("paper_position_closed", payload)

        type(self)._position = PositionState()

        self._sync_state_from_position(state)

        state.position_opened_monotonic_at = None
        state.current_trade_id = None
        state.current_trade_cycle_number = None

        # После закрытия очищаем autonomous cooldown/action,
        # чтобы новая сделка не унаследовала runtime-действие прошлой позиции.
        reset_autonomous_runtime_state(state)

        # После закрытия очищаем protection и lifecycle runtime закрытой позиции.
        self._reset_runtime_protection_state(state)
        self._reset_position_lifecycle_state(state)

        reset_execution_block_state(state)
        state.last_flip_block_reason = None

        state.last_execution_action = (
            f"{EXECUTION_ACTION_FORCE_CLOSE_PREFIX}{forced_reason}"
            if forced_reason is not None
            else EXECUTION_ACTION_CLOSE
        )

        state.last_execution_reason = (
            f"Позиция закрыта по правилу защиты: {forced_reason}."
            if forced_reason is not None
            else "Позиция закрыта."
        )

        type(self)._last_flip_block_key = None

        if forced_reason is not None:
            return ExecutionDecision(
                f"{EXECUTION_ACTION_FORCE_CLOSE_PREFIX}{forced_reason}",
                True,
                f"Позиция закрыта по правилу защиты: {forced_reason}.",
            )

        return ExecutionDecision(
            EXECUTION_ACTION_CLOSE,
            True,
            "Позиция закрыта.",
        )