from __future__ import annotations

import uuid
from decimal import Decimal, ROUND_DOWN
from typing import Any

from .futures_models import FuturesAction, FuturesExecutionResult, FuturesOrderSide, FuturesRiskDecision, FuturesSymbolMetadata
from .models import TradingMode
from .storage import Storage


def build_futures_client_order_id(prefix: str = "bqf") -> str:
    return f"{prefix}-{uuid.uuid4().hex[:24]}"


def round_down(value: Decimal, increment: Decimal) -> Decimal:
    if increment <= 0:
        return value
    return (value / increment).to_integral_value(rounding=ROUND_DOWN) * increment


def _is_positive_finite(value: Decimal) -> bool:
    return value.is_finite() and value > 0


def _sanitized_error_payload(error: Exception, message: str) -> dict[str, str]:
    return {"type": type(error).__name__, "message": message}


def _order_status(payload: dict[str, Any]) -> str:
    return str(payload.get("status", "")).upper()


class FuturesExecutionEngine:
    def __init__(self, mode: TradingMode, exchange_client, storage: Storage) -> None:
        self.mode = mode
        self.exchange_client = exchange_client
        self.storage = storage

    def execute_open(
        self,
        run_id: int,
        decision: FuturesRiskDecision,
        metadata: FuturesSymbolMetadata,
        *,
        reference_price: Decimal,
        current_margin_type: str | None = None,
    ) -> FuturesExecutionResult:
        prediction = decision.prediction
        leverage = decision.leverage or prediction.leverage or 1
        notional = decision.notional or Decimal("0")
        if not decision.approved:
            return self._result(run_id, "rejected", decision.reason, decision, leverage)
        if prediction.action not in {FuturesAction.OPEN_LONG, FuturesAction.OPEN_SHORT}:
            return self._result(run_id, "rejected", "unsupported futures open action", decision, leverage)
        if not _is_positive_finite(reference_price):
            return self._result(run_id, "rejected", "invalid reference price", decision, leverage)
        if not _is_positive_finite(metadata.step_size) or not _is_positive_finite(notional):
            return self._result(run_id, "rejected", "invalid execution quantity", decision, leverage)
        quantity = round_down(notional / reference_price, metadata.step_size)
        if quantity <= 0:
            return self._result(run_id, "rejected", "invalid execution quantity", decision, leverage)
        open_side = FuturesOrderSide.BUY if prediction.action is FuturesAction.OPEN_LONG else FuturesOrderSide.SELL
        close_side = FuturesOrderSide.SELL if open_side is FuturesOrderSide.BUY else FuturesOrderSide.BUY
        if self.mode is TradingMode.DRY_RUN:
            self.storage.record_futures_order_event(
                run_id,
                "dry_run_open",
                prediction.symbol,
                None,
                None,
                "DRY_RUN",
                {
                    "quantity": quantity,
                    "reference_price": reference_price,
                    "notional": notional,
                    "leverage": leverage,
                    "open_side": open_side.value,
                    "close_side": close_side.value,
                    "stop_loss_price": prediction.stop_loss_price,
                    "take_profit_price": prediction.take_profit_price,
                },
            )
            return self._result(run_id, "dry_run", "dry run", decision, leverage)

        if current_margin_type is None or current_margin_type.upper() != "ISOLATED":
            self.exchange_client.change_margin_type(prediction.symbol, "ISOLATED")
        self.exchange_client.change_leverage(prediction.symbol, leverage)
        open_client_order_id = build_futures_client_order_id("bqf-open")
        try:
            open_payload = self.exchange_client.create_market_order(
                prediction.symbol,
                open_side,
                quantity,
                open_client_order_id,
                reduce_only=False,
            )
        except Exception as open_error:
            self.storage.record_futures_order_event(
                run_id,
                "open_unknown",
                prediction.symbol,
                open_client_order_id,
                None,
                "UNKNOWN",
                _sanitized_error_payload(open_error, "open order status unknown"),
            )
            return self._result(run_id, "open_unknown", "open order status unknown", decision, leverage)
        self.storage.record_futures_order_event(
            run_id,
            "open",
            prediction.symbol,
            open_client_order_id,
            None,
            open_payload.get("status"),
            open_payload,
        )
        if _order_status(open_payload) != "FILLED":
            try:
                open_payload = self.exchange_client.query_order(prediction.symbol, open_client_order_id)
            except Exception as query_error:
                self.storage.record_futures_order_event(
                    run_id,
                    "open_query_unknown",
                    prediction.symbol,
                    open_client_order_id,
                    None,
                    "UNKNOWN",
                    _sanitized_error_payload(query_error, "open order status unknown"),
                )
                return self._result(run_id, "open_unknown", "open order status unknown", decision, leverage)
            self.storage.record_futures_order_event(
                run_id,
                "open_query",
                prediction.symbol,
                open_client_order_id,
                None,
                open_payload.get("status"),
                open_payload,
            )
            if _order_status(open_payload) != "FILLED":
                return self._result(run_id, "open_unknown", "open order status unknown", decision, leverage)
        try:
            stop_algo_id = build_futures_client_order_id("bqf-stop")
            stop_payload = self.exchange_client.create_conditional_algo_order(
                prediction.symbol,
                close_side,
                "STOP_MARKET",
                prediction.stop_loss_price,
                quantity,
                stop_algo_id,
            )
            self.storage.record_futures_order_event(
                run_id,
                "stop_algo",
                prediction.symbol,
                None,
                stop_algo_id,
                stop_payload.get("status"),
                stop_payload,
            )
            take_algo_id = build_futures_client_order_id("bqf-tp")
            take_payload = self.exchange_client.create_conditional_algo_order(
                prediction.symbol,
                close_side,
                "TAKE_PROFIT_MARKET",
                prediction.take_profit_price,
                quantity,
                take_algo_id,
            )
            self.storage.record_futures_order_event(
                run_id,
                "take_profit_algo",
                prediction.symbol,
                None,
                take_algo_id,
                take_payload.get("status"),
                take_payload,
            )
        except Exception:
            rollback_id = build_futures_client_order_id("bqf-rb")
            try:
                rollback_payload = self.exchange_client.create_market_order(
                    prediction.symbol,
                    close_side,
                    quantity,
                    rollback_id,
                    reduce_only=True,
                )
            except Exception as rollback_error:
                self.storage.record_futures_order_event(
                    run_id,
                    "rollback_failed",
                    prediction.symbol,
                    rollback_id,
                    None,
                    "CRITICAL",
                    _sanitized_error_payload(rollback_error, "rollback request failed"),
                )
                return self._result(run_id, "rollback_failed", "protection failed; rollback failed", decision, leverage)
            else:
                self.storage.record_futures_order_event(
                    run_id,
                    "rollback",
                    prediction.symbol,
                    rollback_id,
                    None,
                    rollback_payload.get("status"),
                    rollback_payload,
                )
                return self._result(run_id, "rollback_completed", "protection failed; rollback completed", decision, leverage)

        return self._result(run_id, "protected", "protected", decision, leverage)

    def execute_close(
        self,
        run_id: int,
        prediction,
        metadata: FuturesSymbolMetadata,
        *,
        position_quantity: Decimal,
        protection_client_algo_ids: tuple[str, ...],
        existing_stop_loss_price: Decimal | None,
        existing_take_profit_price: Decimal | None,
    ) -> FuturesExecutionResult:
        if prediction.action is not FuturesAction.CLOSE:
            return self._close_result(run_id, "rejected", "unsupported futures close action", prediction)
        if prediction.close_percent not in {25, 50, 100}:
            return self._close_result(run_id, "rejected", "invalid close percent", prediction)
        if not _is_positive_finite(metadata.step_size) or not position_quantity.is_finite() or position_quantity == 0:
            return self._close_result(run_id, "rejected", "invalid close quantity", prediction)

        close_percent = Decimal(prediction.close_percent)
        close_quantity = round_down(abs(position_quantity) * close_percent / Decimal("100"), metadata.step_size)
        if close_quantity <= 0:
            return self._close_result(run_id, "rejected", "invalid close quantity", prediction)

        close_side = FuturesOrderSide.SELL if position_quantity > 0 else FuturesOrderSide.BUY
        remaining_quantity = round_down(abs(position_quantity) - close_quantity, metadata.step_size)
        is_full_close = prediction.close_percent == 100 or remaining_quantity <= 0

        if self.mode is TradingMode.DRY_RUN:
            self.storage.record_futures_order_event(
                run_id,
                "dry_run_close",
                prediction.symbol,
                None,
                None,
                "DRY_RUN",
                {
                    "quantity": close_quantity,
                    "position_quantity": position_quantity,
                    "remaining_quantity": remaining_quantity,
                    "close_percent": prediction.close_percent,
                    "close_side": close_side.value,
                },
            )
            return self._close_result(run_id, "dry_run", "dry run", prediction)

        for client_algo_id in protection_client_algo_ids:
            try:
                cancel_payload = self.exchange_client.cancel_algo_order(client_algo_id)
            except Exception as cancel_error:
                self.storage.record_futures_order_event(
                    run_id,
                    "cancel_algo_failed",
                    prediction.symbol,
                    None,
                    client_algo_id,
                    "FAILED",
                    _sanitized_error_payload(cancel_error, "protection cancel failed"),
                )
                return self._close_result(run_id, "close_failed", "protection cancel failed", prediction)
            self.storage.record_futures_order_event(
                run_id,
                "cancel_algo",
                prediction.symbol,
                None,
                client_algo_id,
                cancel_payload.get("status"),
                cancel_payload,
            )

        close_client_order_id = build_futures_client_order_id("bqf-close")
        try:
            close_payload = self.exchange_client.create_market_order(
                prediction.symbol,
                close_side,
                close_quantity,
                close_client_order_id,
                reduce_only=True,
            )
        except Exception as close_error:
            self.storage.record_futures_order_event(
                run_id,
                "close_unknown",
                prediction.symbol,
                close_client_order_id,
                None,
                "UNKNOWN",
                _sanitized_error_payload(close_error, "close order status unknown"),
            )
            return self._close_result(run_id, "close_unknown", "close order status unknown", prediction)

        self.storage.record_futures_order_event(
            run_id,
            "close",
            prediction.symbol,
            close_client_order_id,
            None,
            close_payload.get("status"),
            close_payload,
        )

        if is_full_close:
            return self._close_result(run_id, "closed", "closed", prediction)

        if existing_stop_loss_price is None or existing_take_profit_price is None:
            self._record_close_protection_failed(
                run_id,
                prediction.symbol,
                None,
                {"message": "missing remaining protection prices"},
            )
            return self._close_result(
                run_id,
                "close_protection_failed",
                "close completed; remaining protection missing",
                prediction,
            )

        try:
            stop_algo_id = build_futures_client_order_id("bqf-stop")
            stop_payload = self.exchange_client.create_conditional_algo_order(
                prediction.symbol,
                close_side,
                "STOP_MARKET",
                existing_stop_loss_price,
                remaining_quantity,
                stop_algo_id,
            )
            self.storage.record_futures_order_event(
                run_id,
                "stop_algo",
                prediction.symbol,
                None,
                stop_algo_id,
                stop_payload.get("status"),
                stop_payload,
            )
            take_algo_id = build_futures_client_order_id("bqf-tp")
            take_payload = self.exchange_client.create_conditional_algo_order(
                prediction.symbol,
                close_side,
                "TAKE_PROFIT_MARKET",
                existing_take_profit_price,
                remaining_quantity,
                take_algo_id,
            )
            self.storage.record_futures_order_event(
                run_id,
                "take_profit_algo",
                prediction.symbol,
                None,
                take_algo_id,
                take_payload.get("status"),
                take_payload,
            )
        except Exception as protection_error:
            self._record_close_protection_failed(
                run_id,
                prediction.symbol,
                None,
                _sanitized_error_payload(protection_error, "remaining protection request failed"),
            )
            return self._close_result(
                run_id,
                "close_protection_failed",
                "close completed; remaining protection failed",
                prediction,
            )

        return self._close_result(run_id, "partially_closed", "partially closed", prediction)

    def _record_close_protection_failed(
        self,
        run_id: int,
        symbol: str,
        client_algo_id: str | None,
        raw: dict[str, Any],
    ) -> None:
        self.storage.record_futures_order_event(
            run_id,
            "close_protection_failed",
            symbol,
            None,
            client_algo_id,
            "FAILED",
            raw,
        )

    def _result(
        self,
        run_id: int,
        status: str,
        reason: str,
        decision: FuturesRiskDecision,
        leverage: int,
    ) -> FuturesExecutionResult:
        prediction = decision.prediction
        return FuturesExecutionResult(
            run_id,
            status,
            prediction.symbol,
            prediction.action.value,
            reason,
            prediction.confidence,
            leverage,
            decision.margin_amount,
        )

    def _close_result(
        self,
        run_id: int,
        status: str,
        reason: str,
        prediction,
    ) -> FuturesExecutionResult:
        return FuturesExecutionResult(
            run_id,
            status,
            prediction.symbol,
            prediction.action.value,
            reason,
            prediction.confidence,
            prediction.leverage,
            close_percent=prediction.close_percent,
        )
