import asyncio
import traceback
from typing import Any

import pandas as pd

import ccxt
from ccxt.base.errors import ExchangeError
from qubx import logger
from qubx.core.basics import (
    CtrlChannel,
    Instrument,
    Order,
    OrderSide,
)
from qubx.core.errors import ErrorLevel, OrderCancellationError, OrderCreationError, create_error_event
from qubx.core.exceptions import BadRequest, InvalidOrderParameters, OrderNotFound
from qubx.core.interfaces import (
    IAccountProcessor,
    IBroker,
    IDataProvider,
    ITimeProvider,
)
from qubx.utils.misc import AsyncThreadLoop

from .exchange_manager import ExchangeManager
from .utils import ccxt_convert_order_info, instrument_to_ccxt_symbol


class CcxtBroker(IBroker):
    _exchange_manager: ExchangeManager

    def __init__(
        self,
        exchange_manager: ExchangeManager,
        channel: CtrlChannel,
        time_provider: ITimeProvider,
        account: IAccountProcessor,
        data_provider: IDataProvider,
        cancel_timeout: int = 30,
        cancel_retry_interval: int = 2,
        max_cancel_retries: int = 10,
        enable_create_order_ws: bool = False,
        enable_cancel_order_ws: bool = False,
        enable_edit_order_ws: bool = False,
    ):
        self._exchange_manager = exchange_manager
        self.ccxt_exchange_id = str(self._exchange_manager.exchange.name)
        self.channel = channel
        self.time_provider = time_provider
        self.account = account
        self.data_provider = data_provider
        self.cancel_timeout = cancel_timeout
        self.cancel_retry_interval = cancel_retry_interval
        self.max_cancel_retries = max_cancel_retries
        self.enable_create_order_ws = enable_create_order_ws
        self.enable_cancel_order_ws = enable_cancel_order_ws
        self.enable_edit_order_ws = enable_edit_order_ws

    @property
    def _loop(self) -> AsyncThreadLoop:
        """Get current AsyncThreadLoop for the exchange."""
        return AsyncThreadLoop(self._exchange_manager.exchange.asyncio_loop)

    @property
    def is_simulated_trading(self) -> bool:
        return False

    def _post_order_error_to_databus(
        self,
        error: Exception,
        instrument: Instrument,
        order_side: OrderSide,
        order_type: str,
        amount: float,
        price: float | None,
        client_id: str | None,
        time_in_force: str,
        **options,
    ):
        level = ErrorLevel.LOW
        match error:
            case ccxt.InsufficientFunds():
                level = ErrorLevel.HIGH
                logger.error(
                    f"(::create_order) INSUFFICIENT FUNDS for {order_side} {amount} {order_type} for {instrument.symbol} : {error}"
                )
            case ccxt.OrderNotFillable():
                level = ErrorLevel.LOW
                logger.error(
                    f"(::create_order) ORDER NOT FILLEABLE for {order_side} {amount} {order_type} for [{instrument.symbol}] : {error}"
                )
            case ccxt.InvalidOrder():
                level = ErrorLevel.LOW
                logger.error(
                    f"(::create_order) INVALID ORDER for {order_side} {amount} {order_type} for {instrument.symbol} : {error}"
                )
            case ccxt.BadRequest():
                level = ErrorLevel.LOW
                logger.error(
                    f"(::create_order) BAD REQUEST for {order_side} {amount} {order_type} for {instrument.symbol} : {error}"
                )
            case _:
                level = ErrorLevel.MEDIUM
                logger.error(f"(::create_order) Unexpected error: {error}")

        error_event = OrderCreationError(
            timestamp=self.time_provider.time(),
            message=f"Error message: {str(error)}",
            level=level,
            instrument=instrument,
            amount=amount,
            price=price,
            order_type=order_type,
            side=order_side,
            error=error,
        )
        self.channel.send(create_error_event(error_event))

    def send_order_async(
        self,
        instrument: Instrument,
        order_side: OrderSide,
        order_type: str,
        amount: float,
        price: float | None = None,
        client_id: str | None = None,
        time_in_force: str = "gtc",
        **options,
    ) -> Any:  # Return type as Any to avoid Future/Task typing issues
        """
        Submit an order asynchronously. Errors will be sent through the channel.

        Returns:
            Future-like object that will eventually contain the result
        """

        async def _execute_order_with_channel_errors():
            try:
                order, error = await self._create_order(
                    instrument=instrument,
                    order_side=order_side,
                    order_type=order_type,
                    amount=amount,
                    price=price,
                    client_id=client_id,
                    time_in_force=time_in_force,
                    **options,
                )

                if error:
                    self._post_order_error_to_databus(
                        error, instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
                    )
                    order = None

                return order

            except Exception as err:
                # Catch any unexpected errors and send them through the channel as well
                logger.error(f"{self.__class__.__name__} :: Unexpected error in async order creation: {err}")
                logger.error(traceback.format_exc())
                self._post_order_error_to_databus(
                    err, instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
                )
                return None

        # Submit the task to the async loop
        return self._loop.submit(_execute_order_with_channel_errors())

    def send_order(
        self,
        instrument: Instrument,
        order_side: OrderSide,
        order_type: str,
        amount: float,
        price: float | None = None,
        client_id: str | None = None,
        time_in_force: str = "gtc",
        **options,
    ) -> Order:
        """
        Submit an order and wait for the result. Exceptions will be raised on errors.

        Returns:
            Order: The created order object

        Raises:
            Various exceptions based on the error that occurred
        """
        try:
            # Create a task that executes the order creation
            future = self._loop.submit(
                self._create_order(
                    instrument=instrument,
                    order_side=order_side,
                    order_type=order_type,
                    amount=amount,
                    price=price,
                    client_id=client_id,
                    time_in_force=time_in_force,
                    **options,
                )
            )

            # Wait for the result
            order, error = future.result()

            # If there was an error, raise it
            if error:
                raise error

            # If there was no error but also no order, something went wrong
            if not order and not self.enable_create_order_ws:
                raise ExchangeError(f"{self.__class__.__name__} :: Order creation failed with no specific error")

            return order

        except Exception as err:
            # This will catch any errors from future.result() or if we explicitly raise an error
            self._post_order_error_to_databus(
                err, instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
            )
            raise err

    def cancel_order(self, order_id: str) -> bool:
        """Cancel an order synchronously and return success status."""
        orders = self.account.get_orders()
        if order_id not in orders:
            logger.warning(f"Order {order_id} not found in active orders")
            return False

        order = orders[order_id]
        logger.info(f"Canceling order {order_id} synchronously...")

        try:
            # Submit the task and wait for result
            future = self._loop.submit(self._cancel_order_with_retry(order_id, order.instrument))
            return future.result()  # This will block until completion or timeout
        except Exception as e:
            logger.error(f"Error during synchronous order cancellation: {e}")
            return False  # Return False on any error for simplicity

    def cancel_order_async(self, order_id: str) -> None:
        """Cancel an order asynchronously (non blocking)."""
        orders = self.account.get_orders()
        if order_id not in orders:
            logger.warning(f"Order {order_id} not found in active orders")
            return

        order = orders[order_id]
        logger.info(f"Canceling order {order_id} asynchronously...")

        # Submit the task without waiting for result
        self._loop.submit(self._cancel_order_with_retry(order_id, order.instrument))

    async def _create_order(
        self,
        instrument: Instrument,
        order_side: OrderSide,
        order_type: str,
        amount: float,
        price: float | None = None,
        client_id: str | None = None,
        time_in_force: str = "gtc",
        **options,
    ) -> tuple[Order | None, Exception | None]:
        """
        Asynchronously create an order with the exchange.

        Returns:
            tuple: (Order object if successful, Exception if failed)
        """
        try:
            payload = self._prepare_order_payload(
                instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
            )
            if self.enable_create_order_ws:
                r = await self._exchange_manager.exchange.create_order_ws(**payload)
            else:
                r = await self._exchange_manager.exchange.create_order(**payload)

            if r is None:
                msg = "(::_create_order) No response from exchange"
                logger.error(msg)
                return None, ExchangeError(msg)

            if r["id"] is None:
                return None, None

            order = ccxt_convert_order_info(instrument, r)
            logger.info(f"New order {order}")
            return order, None

        except Exception as err:
            return None, err

    def _prepare_order_payload(
        self,
        instrument: Instrument,
        order_side: OrderSide,
        order_type: str,
        amount: float,
        price: float | None = None,
        client_id: str | None = None,
        time_in_force: str = "gtc",
        **options,
    ) -> dict[str, Any]:
        params = {}
        _is_trigger_order = order_type.startswith("stop_")

        quote = self.data_provider.get_quote(instrument)
        if quote is None:
            logger.warning(f"[<y>{instrument.symbol}</y>] :: Quote is not available for order creation.")
            raise BadRequest(f"Quote is not available for order creation for {instrument.symbol}")

        # Auto-detect if order reduces existing position
        reduce_only = options.get("reduceOnly", False)
        if not reduce_only:
            positions = self.account.get_positions()
            if instrument in positions:
                position_qty = positions[instrument].quantity
                # Check if order closes position AND doesn't exceed position size (which would flip to opposite side)
                if (position_qty > 0 and order_side == "SELL" and abs(amount) <= abs(position_qty)) or (
                    position_qty < 0 and order_side == "BUY" and abs(amount) <= abs(position_qty)
                ):
                    reduce_only = True
                    logger.debug(
                        f"[{instrument.symbol}] Auto-setting reduceOnly=True ({order_side}, position: {position_qty})"
                    )

        if not reduce_only:
            min_notional = instrument.min_notional
            if min_notional > 0 and abs(amount) * quote.mid_price() < min_notional:
                raise InvalidOrderParameters(
                    f"[{instrument.symbol}] Order amount {amount} is too small. Minimum notional is {min_notional}"
                )
        else:
            params["reduceOnly"] = reduce_only

        # - handle trigger (stop) orders
        if _is_trigger_order:
            params["triggerPrice"] = price
            order_type = order_type.split("_")[1]

        if client_id:
            params["newClientOrderId"] = client_id

        if instrument.is_futures():
            params["type"] = "swap"

        ccxt_symbol = instrument_to_ccxt_symbol(instrument)

        if order_type == "limit" or _is_trigger_order:
            time_in_force = time_in_force.upper()
            params["timeInForce"] = time_in_force
            if price is None:
                raise InvalidOrderParameters(f"Price must be specified for '{order_type}' order")
            if order_side == "BUY" and time_in_force == "GTX" and price >= quote.ask:
                logger.info(
                    f"[{instrument.symbol}] :: GTX BUY order price {price} is greater than ask price {quote.ask}. Setting 1 tick below ask."
                )
                price = quote.ask - instrument.tick_size
            elif order_side == "SELL" and time_in_force == "GTX" and price <= quote.bid:
                logger.info(
                    f"[{instrument.symbol}] :: GTX SELL order price {price} is less than bid price {quote.bid}. Setting 1 tick above bid."
                )
                price = quote.bid + instrument.tick_size

        return {
            "symbol": ccxt_symbol,
            "type": order_type.lower(),
            "side": order_side.lower(),
            "amount": amount,
            "price": price,
            "params": params,
        }

    async def _cancel_order_with_retry(self, order_id: str, instrument: Instrument) -> bool:
        """
        Attempts to cancel an order with retries.

        Args:
            order_id: The ID of the order to cancel
            symbol: The symbol of the instrument

        Returns:
            bool: True if cancellation was successful, False otherwise
        """
        start_time = self.time_provider.time()
        timeout_delta = self.cancel_timeout
        retries = 0

        while True:
            try:
                if self.enable_cancel_order_ws:
                    await self._exchange_manager.exchange.cancel_order_ws(
                        order_id, symbol=instrument_to_ccxt_symbol(instrument)
                    )
                else:
                    await self._exchange_manager.exchange.cancel_order(
                        order_id, symbol=instrument_to_ccxt_symbol(instrument)
                    )
                return True
            except ccxt.OperationRejected as err:
                err_msg = str(err).lower()
                # Check if the error is about an unknown order or non-existent order
                if "unknown order" in err_msg or "order does not exist" in err_msg or "order not found" in err_msg:
                    # These errors might be temporary if the order is still being processed, so retry
                    logger.debug(f"[{order_id}] Order not found for cancellation, might retry: {err}")
                    # Continue with the retry logic instead of returning immediately
                # Order cannot be cancelled (e.g., already filled)
                elif "filled" in err_msg or "partially filled" in err_msg:
                    logger.debug(f"[{order_id}] Order cannot be cancelled - already executed: {err}")
                    return False  # FAILURE: Order cannot be cancelled
                # Other operation rejected errors - don't retry
                else:
                    logger.debug(f"[{order_id}] Could not cancel order: {err}")
                    return False
            except (ccxt.NetworkError, ccxt.ExchangeError, ccxt.ExchangeNotAvailable) as e:
                logger.warning(f"[{order_id}] Network or exchange error while cancelling: {e}")
                # Continue with retry logic
            except Exception as err:
                logger.error(f"Unexpected error canceling order {order_id}: {err}")
                return False

            # Common retry logic for all retryable errors
            current_time = self.time_provider.time()
            elapsed_seconds = pd.Timedelta(current_time - start_time).total_seconds()
            retries += 1

            if elapsed_seconds >= timeout_delta or retries >= self.max_cancel_retries:
                logger.error(f"Timeout reached for canceling order {order_id}")
                self.channel.send(
                    create_error_event(
                        OrderCancellationError(
                            timestamp=self.time_provider.time(),
                            order_id=order_id,
                            message=f"Timeout reached for canceling order {order_id}",
                            instrument=instrument,
                            level=ErrorLevel.LOW,
                            error=None,
                        )
                    )
                )
                return False

            # Wait before retrying with exponential backoff
            backoff_time = min(self.cancel_retry_interval * (2 ** (retries - 1)), 30)
            logger.debug(f"Retrying order cancellation for {order_id} in {backoff_time} seconds (retry {retries})")
            await asyncio.sleep(backoff_time)

        # This should never be reached due to the return statements above,
        # but it's here to satisfy the type checker
        return False

    def cancel_orders(self, instrument: Instrument) -> None:
        orders = self.account.get_orders()
        instrument_orders = [order_id for order_id, order in orders.items() if order.instrument == instrument]

        # Submit all cancellations without waiting for results
        for order_id in instrument_orders:
            self.cancel_order(order_id)

    def update_order(self, order_id: str, price: float, amount: float) -> Order:
        """Update an existing limit order with new price and amount.

        Args:
            order_id: The ID of the order to update
            price: New price for the order (already adjusted by TradingManager)
            amount: New amount for the order (already adjusted by TradingManager)

        Returns:
            Order: The updated Order object if successful

        Raises:
            OrderNotFound: If the order is not found
            BadRequest: If the order is not a limit order
            ExchangeError: If the exchange operation fails
        """
        active_orders = self.account.get_orders()
        if order_id not in active_orders:
            raise OrderNotFound(f"Order {order_id} not found in active orders")

        existing_order = active_orders[order_id]

        # Validate that the order can still be updated (not fully filled/closed)
        updatable_statuses = ["OPEN", "NEW", "PENDING"]
        if existing_order.status not in updatable_statuses:
            raise BadRequest(
                f"Order {order_id} with status '{existing_order.status}' cannot be updated. "
                f"Only orders with status {updatable_statuses} can be updated."
            )

        instrument = existing_order.instrument

        logger.debug(
            f"[<g>{instrument.symbol}</g>] :: Updating order {order_id}: "
            f"{amount} @ {price} (was: {existing_order.quantity} @ {existing_order.price} ({existing_order.time_in_force}))"
        )

        try:
            # Check if exchange supports order editing
            if self._exchange_manager.exchange.has.get("editOrder", False):
                return self._update_order_direct(order_id, existing_order, price, amount)
            else:
                return self._update_order_fallback(order_id, existing_order, price, amount)
        except Exception as err:
            logger.error(f"Failed to update order {order_id}: {err}")
            raise

    def _update_order_direct(self, order_id: str, existing_order: Order, price: float, amount: float) -> Order:
        """Update order using exchange's native edit functionality."""
        future_result = self._loop.submit(self._edit_order_async(order_id, existing_order, price, amount))
        updated_order, error = future_result.result()

        if error is not None:
            raise error

        if updated_order is not None:
            self.account.process_order(updated_order)
            logger.debug(f"[<g>{existing_order.instrument.symbol}</g>] :: Successfully updated order {order_id}")
            return updated_order
        else:
            raise Exception("Order update returned None without error")

    def _update_order_fallback(self, order_id: str, existing_order: Order, price: float, amount: float) -> Order:
        """Update order using cancel+recreate strategy for exchanges without editOrder support."""
        success = self.cancel_order(order_id)
        if not success:
            raise Exception(f"Failed to cancel order {order_id} during update")

        updated_order = self.send_order(
            instrument=existing_order.instrument,
            order_side=existing_order.side,
            order_type=existing_order.type,
            amount=amount,
            price=price,
            client_id=existing_order.client_id,  # Preserve original client_id for tracking
            time_in_force=existing_order.time_in_force or "gtc",
        )

        logger.debug(
            f"[<g>{existing_order.instrument.symbol}</g>] :: Successfully updated order {order_id} -> new order {updated_order.id}"
        )
        return updated_order

    async def _edit_order_async(
        self, order_id: str, existing_order: Order, price: float, amount: float
    ) -> tuple[Order | None, Exception | None]:
        """Async helper for direct order editing."""
        try:
            ccxt_symbol = instrument_to_ccxt_symbol(existing_order.instrument)
            ccxt_side = "buy" if existing_order.side == "BUY" else "sell"

            # CCXT requires positive amount (side determines direction)
            abs_amount = abs(amount)

            # Use WebSocket if enabled, otherwise use REST API
            if self.enable_edit_order_ws:
                result = await self._exchange_manager.exchange.edit_order_ws(
                    id=order_id, symbol=ccxt_symbol, type="limit", side=ccxt_side, amount=abs_amount, price=price, params={}
                )
            else:
                result = await self._exchange_manager.exchange.edit_order(
                    id=order_id, symbol=ccxt_symbol, type="limit", side=ccxt_side, amount=abs_amount, price=price, params={}
                )

            # Convert the result back to our Order format
            updated_order = ccxt_convert_order_info(existing_order.instrument, result)
            return updated_order, None

        except Exception as err:
            logger.error(f"Async edit order failed for {order_id}: {err}")
            return None, err

    def exchange(self) -> str:
        """
        Return the name of the exchange this broker is connected to.
        """
        return self.ccxt_exchange_id.upper()
