import asyncio
import msgspec
from typing import Dict, List
from decimal import Decimal

from nexustrader.error import (
    PositionModeError,
    WsRequestNotSentError,
    WsAckTimeoutError,
    WsAckRejectedError,
)
from nexustrader.constants import WsOrderResultType

from nexustrader.core.nautilius_core import LiveClock, MessageBus
from nexustrader.base import OrderManagementSystem
from nexustrader.core.entity import TaskManager
from nexustrader.core.registry import OrderRegistry
from nexustrader.core.cache import AsyncCache
from nexustrader.schema import (
    Order,
    Position,
    BatchOrderSubmit,
)
from nexustrader.constants import (
    OrderSide,
    OrderStatus,
    ExchangeType,
    OrderType,
    TimeInForce,
    PositionSide,
    TriggerType,
)
from nexustrader.exchange.bybit.schema import (
    BybitWsMessageGeneral,
    BybitWsApiGeneralMsg,
    BybitWsOrderMsg,
    BybitMarket,
    BybitWsPositionMsg,
    BybitWsAccountWalletMsg,
    BybitWalletBalanceResponse,
    BybitPositionStruct,
    BybitWsApiOrderMsg,
    BybitOrder as BybitRestOrder,
)
from nexustrader.exchange.bybit.rest_api import BybitApiClient
from nexustrader.exchange.bybit.websockets import BybitWSClient, BybitWSApiClient
from nexustrader.exchange.bybit.constants import (
    BybitAccountType,
    BybitEnumParser,
    BybitProductType,
    BybitOrderType,
    BybitTimeInForce,
)


class BybitOrderManagementSystem(OrderManagementSystem):
    _ws_client: BybitWSClient
    _account_type: BybitAccountType
    _market: Dict[str, BybitMarket]
    _market_id: Dict[str, str]
    _api_client: BybitApiClient

    def __init__(
        self,
        account_type: BybitAccountType,
        api_key: str,
        secret: str,
        market: Dict[str, BybitMarket],
        market_id: Dict[str, str],
        registry: OrderRegistry,
        cache: AsyncCache,
        api_client: BybitApiClient,
        exchange_id: ExchangeType,
        clock: LiveClock,
        msgbus: MessageBus,
        task_manager: TaskManager,
        enable_rate_limit: bool,
    ):
        super().__init__(
            account_type=account_type,
            market=market,
            market_id=market_id,
            registry=registry,
            cache=cache,
            api_client=api_client,
            ws_client=BybitWSClient(
                account_type=account_type,
                handler=self._ws_msg_handler,
                task_manager=task_manager,
                clock=clock,
                api_key=api_key,
                secret=secret,
            ),
            exchange_id=exchange_id,
            clock=clock,
            msgbus=msgbus,
            task_manager=task_manager,
        )

        self._ws_api_client = BybitWSApiClient(
            account_type=account_type,
            api_key=api_key,
            secret=secret,
            handler=self._ws_api_msg_handler,
            task_manager=task_manager,
            clock=clock,
            enable_rate_limit=enable_rate_limit,
        )

        self._ws_api_msg_general_decoder = msgspec.json.Decoder(BybitWsApiGeneralMsg)
        self._ws_api_msg_order_decoder = msgspec.json.Decoder(BybitWsApiOrderMsg)
        self._ws_msg_general_decoder = msgspec.json.Decoder(BybitWsMessageGeneral)
        self._ws_msg_order_update_decoder = msgspec.json.Decoder(BybitWsOrderMsg)
        self._ws_msg_position_decoder = msgspec.json.Decoder(BybitWsPositionMsg)
        self._ws_msg_wallet_decoder = msgspec.json.Decoder(BybitWsAccountWalletMsg)
        self._pending_ws_acks: Dict[str, asyncio.Future] = {}

        self._ws_api_client.set_lifecycle_hooks(
            on_disconnected=self._on_ws_api_disconnected,
        )

    def _reject_all_pending_ws_acks(self):
        pending = list(self._pending_ws_acks.items())
        self._pending_ws_acks.clear()
        for oid, fut in pending:
            if not fut.done():
                fut.set_exception(
                    WsRequestNotSentError(
                        "WebSocket API connection lost while waiting for ACK"
                    )
                )
        if pending:
            self._log.warning(
                f"Rejected {len(pending)} pending WS ACK(s) due to WS API disconnect"
            )

    def _resolve_ws_ack(self, oid: str):
        fut = self._pending_ws_acks.pop(oid, None)
        if fut and not fut.done():
            fut.set_result(True)

    def _reject_ws_ack(self, oid: str, reason: str):
        fut = self._pending_ws_acks.pop(oid, None)
        if fut and not fut.done():
            fut.set_exception(WsAckRejectedError(oid=oid, reason=reason))

    def _parse_order_create(self, raw: bytes):
        msg = self._ws_api_msg_order_decoder.decode(raw)
        oid = msg.oid

        tmp_order = self._registry.get_tmp_order(oid)
        if not tmp_order:
            return
        symbol = tmp_order.symbol
        amount = tmp_order.amount
        type = tmp_order.type
        price = tmp_order.price
        side = tmp_order.side
        time_in_force = tmp_order.time_in_force
        reduce_only = tmp_order.reduce_only
        ts = self._clock.timestamp_ms()
        if msg.is_success:
            ordId = msg.data.orderId
            self._log.debug(f"[{symbol}] new order success: oid: {oid} eid: {ordId}")
            order = Order(
                exchange=self._exchange_id,
                symbol=symbol,
                oid=oid,
                eid=ordId,
                side=side,
                status=OrderStatus.PENDING,
                amount=amount,
                timestamp=ts,
                type=type,
                price=price,
                time_in_force=time_in_force,
                reduce_only=reduce_only,
            )
            self.order_status_update(order)  # INITIALIZED -> PENDING
            self._resolve_ws_ack(oid)
        else:
            self._log.error(f"[{symbol}] new order failed: oid: {oid} {msg.error_msg}")
            order = Order(
                exchange=self._exchange_id,
                symbol=symbol,
                oid=oid,
                status=OrderStatus.FAILED,
                amount=amount,
                timestamp=ts,
                side=side,
                type=type,
                price=price,
                time_in_force=time_in_force,
                reduce_only=reduce_only,
                reason=msg.error_msg,
            )
            self.order_status_update(order)
            self._reject_ws_ack(oid, msg.error_msg)

    def _parse_order_cancel(self, raw: bytes):
        msg = self._ws_api_msg_order_decoder.decode(raw)
        oid = msg.oid
        tmp_order = self._registry.get_tmp_order(oid)
        if not tmp_order:
            return
        symbol = tmp_order.symbol
        amount = tmp_order.amount
        side = tmp_order.side
        type = tmp_order.type
        price = tmp_order.price
        time_in_force = tmp_order.time_in_force
        reduce_only = tmp_order.reduce_only
        ts = self._clock.timestamp_ms()
        if msg.is_success:
            ordId = msg.data.orderId
            self._log.debug(
                f"[{symbol}] canceling order success: oid: {oid} id: {ordId}"
            )
            order = Order(
                exchange=self._exchange_id,
                symbol=symbol,
                oid=oid,
                eid=ordId,
                side=side,
                status=OrderStatus.CANCELING,
                amount=amount,
                type=type,
                price=price,
                time_in_force=time_in_force,
                timestamp=ts,
                reduce_only=reduce_only,
            )
            self.order_status_update(order)
            self._resolve_ws_ack(oid)
        else:
            self._log.error(
                f"[{symbol}] canceling order failed: oid: {oid} {msg.error_msg}"
            )
            order = Order(
                exchange=self._exchange_id,
                symbol=symbol,
                oid=oid,
                status=OrderStatus.CANCEL_FAILED,
                amount=amount,
                side=side,
                type=type,
                price=price,
                time_in_force=time_in_force,
                timestamp=ts,
                reduce_only=reduce_only,
                reason=msg.error_msg,
            )
            self.order_status_update(order)
            self._reject_ws_ack(oid, msg.error_msg)

    def _ws_api_msg_handler(self, raw: bytes):
        try:
            ws_msg = self._ws_api_msg_general_decoder.decode(raw)
            # if ws_msg.is_pong:
            #     self._ws_api_client._transport.notify_user_specific_pong_received()
            #     self._log.debug(f"Pong received {str(ws_msg)}")
            #     return
            if ws_msg.is_auth:
                if ws_msg.is_success:
                    self._ws_api_client.notify_auth_success()
                else:
                    self._log.error(f"WS API auth failed: {ws_msg.error_msg}")
            elif ws_msg.is_order_create:
                self._parse_order_create(raw)
            elif ws_msg.is_order_cancel:
                self._parse_order_cancel(raw)
        except msgspec.DecodeError as e:
            self._log.error(f"Error decoding message: {str(raw)} {e}")

    def _ws_msg_handler(self, raw: bytes):
        try:
            ws_msg = self._ws_msg_general_decoder.decode(raw)
            # if ws_msg.op == "pong":
            #     self._ws_client._transport.notify_user_specific_pong_received()
            #     self._log.debug(f"Pong received {str(ws_msg)}")
            #     return
            if ws_msg.op == "auth":
                if ws_msg.success:
                    self._ws_client.notify_auth_success()
                else:
                    self._log.error(f"WS auth failed: {ws_msg.ret_msg}")
                return
            if ws_msg.success is False:
                self._log.error(f"WebSocket error: {ws_msg}")
                return
            if "order" in ws_msg.topic:
                self._parse_order_update(raw)
            elif "position" in ws_msg.topic:
                self._parse_position_update(raw)
            elif "wallet" == ws_msg.topic:
                self._parse_wallet_update(raw)
        except msgspec.DecodeError as e:
            self._log.error(f"Error decoding message: {str(raw)} {e}")

    def _get_category(self, market: BybitMarket):
        if market.spot:
            return "spot"
        elif market.linear:
            return "linear"
        elif market.inverse:
            return "inverse"
        else:
            raise ValueError(f"Unsupported market type: {market.type}")

    def _get_symbol_from_rest_order(
        self, bybit_symbol: str, category: BybitProductType
    ) -> str | None:
        if category.is_spot:
            key = f"{bybit_symbol}_spot"
        elif category.is_linear:
            key = f"{bybit_symbol}_linear"
        elif category.is_inverse:
            key = f"{bybit_symbol}_inverse"
        else:
            return None
        return self._market_id.get(key)

    def _convert_rest_order(
        self, data: BybitRestOrder, category: BybitProductType
    ) -> Order | None:
        symbol = self._get_symbol_from_rest_order(data.symbol, category)
        if not symbol:
            return None
        tif = BybitEnumParser.parse_time_in_force(data.timeInForce)
        status = BybitEnumParser.parse_order_status(data.orderStatus)
        order_type = BybitEnumParser.parse_order_type(data.orderType, data.timeInForce)
        filled = Decimal(data.cumExecQty) if data.cumExecQty else Decimal(0)
        amount = Decimal(data.qty)
        avg = float(data.avgPrice) if data.avgPrice else None
        price = float(data.price) if data.price else None
        return Order(
            exchange=self._exchange_id,
            symbol=symbol,
            oid=data.orderLinkId,
            eid=data.orderId,
            status=status,
            side=BybitEnumParser.parse_order_side(data.side),
            type=order_type,
            time_in_force=tif,
            amount=amount,
            filled=filled,
            remaining=Decimal(data.leavesQty) if data.leavesQty else amount - filled,
            price=price,
            average=avg,
            timestamp=int(data.updatedTime)
            if data.updatedTime
            else self._clock.timestamp_ms(),
            fee=Decimal(data.cumExecFee) if data.cumExecFee else Decimal(0),
            cum_cost=Decimal(data.cumExecValue) if data.cumExecValue else Decimal(0),
            reduce_only=data.reduceOnly,
        )

    async def cancel_order_ws(self, oid: str, symbol: str, **kwargs):
        ws_fallback = kwargs.pop("ws_fallback", True)
        market = self._market.get(symbol)
        if not market:
            raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
        id = market.id
        category = self._get_category(market)
        params = {
            "category": category,
            "symbol": id,
            "orderLinkId": oid,
            **kwargs,
        }

        ack_future: asyncio.Future = asyncio.get_event_loop().create_future()
        self._pending_ws_acks[oid] = ack_future
        try:
            await self._ws_api_client.cancel_order(id=oid, **params)
        except WsRequestNotSentError as e:
            self._pending_ws_acks.pop(oid, None)
            self._log.warning(f"[{symbol}] cancel_order_ws send failed: {e}")
            if ws_fallback:
                await self.cancel_order(oid=oid, symbol=symbol, **kwargs)
                return
            else:
                self.order_status_update(
                    Order(
                        oid=oid,
                        exchange=self._exchange_id,
                        timestamp=self._clock.timestamp_ms(),
                        symbol=symbol,
                        status=OrderStatus.CANCEL_FAILED,
                        reason=f"WS_REQUEST_NOT_SENT: {e}",
                    )
                )
                raise

        try:
            await asyncio.wait_for(asyncio.shield(ack_future), timeout=5.0)
        except (asyncio.TimeoutError, WsRequestNotSentError):
            self._pending_ws_acks.pop(oid, None)
            self._log.warning(
                f"[{symbol}] cancel_order_ws ACK not received for oid={oid}, confirming via REST..."
            )
            if not await self._confirm_order_after_ack_timeout(oid, symbol):
                raise WsAckTimeoutError(oid=oid, timeout=5.0)
            return WsOrderResultType.ACK_TIMEOUT_CONFIRMED

    async def cancel_order(self, oid: str, symbol: str, **kwargs):
        market = self._market.get(symbol)
        if not market:
            raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
        id = market.id
        category = self._get_category(market)
        params = {
            "category": category,
            "symbol": id,
            "orderLinkId": oid,
            **kwargs,
        }
        try:
            res = await self._api_client.post_v5_order_cancel(**params)
            order = Order(
                oid=oid,
                eid=res.result.orderId,
                exchange=self._exchange_id,
                timestamp=res.time,
                symbol=symbol,
                status=OrderStatus.CANCELING,
            )
        except Exception as e:
            error_msg = f"{e.__class__.__name__}: {str(e)}"
            self._log.error(f"Error canceling order: {error_msg} params: {str(params)}")
            order = Order(
                oid=oid,
                exchange=self._exchange_id,
                timestamp=self._clock.timestamp_ms(),
                symbol=symbol,
                status=OrderStatus.CANCEL_FAILED,
                reason=error_msg,
            )
        self.order_status_update(order)

    def _init_account_balance(self):
        res: BybitWalletBalanceResponse = self._run_sync(
            self._api_client.get_v5_account_wallet_balance(account_type="UNIFIED")
        )
        for result in res.result.list:
            self._cache._apply_balance(self._account_type, result.parse_to_balances())

    def _get_all_positions_list(
        self, category: BybitProductType, settle_coin: str | None = None
    ) -> list[BybitPositionStruct]:
        all_positions = []
        next_page_cursor = ""

        while True:
            res = self._run_sync(
                self._api_client.get_v5_position_list(
                    category=category.value,
                    settleCoin=settle_coin,
                    limit=200,
                    cursor=next_page_cursor,
                )
            )

            all_positions.extend(res.result.list)

            # If there's no next page cursor, we've reached the end
            if not res.result.nextPageCursor:
                break

            next_page_cursor = res.result.nextPageCursor

        return all_positions

    def _init_position(self):
        res_linear_usdt = self._get_all_positions_list(
            BybitProductType.LINEAR, settle_coin="USDT"
        )
        res_linear_usdc = self._get_all_positions_list(
            BybitProductType.LINEAR, settle_coin="USDC"
        )
        res_inverse = self._get_all_positions_list(BybitProductType.INVERSE)

        self._apply_cache_position(res_linear_usdt, BybitProductType.LINEAR)
        self._apply_cache_position(res_linear_usdc, BybitProductType.LINEAR)
        self._apply_cache_position(res_inverse, BybitProductType.INVERSE)

        self._cache.get_all_positions()

    def _position_mode_check(self):
        # NOTE: no need to implement this for bybit, we do position mode check in _get_all_positions_list
        pass

    def _apply_cache_position(
        self, positions: list[BybitPositionStruct], category: BybitProductType
    ):
        for result in positions:
            side = result.side.parse_to_position_side()
            if side == PositionSide.FLAT:
                signed_amount = Decimal(0)
                side = None
            elif side == PositionSide.LONG:
                signed_amount = Decimal(result.size)
            elif side == PositionSide.SHORT:
                signed_amount = -Decimal(result.size)

            if category.is_inverse:
                id = result.symbol + "_inverse"
            elif category.is_linear:
                id = result.symbol + "_linear"

            symbol = self._market_id[id]

            if not result.positionIdx.is_one_way_mode():
                raise PositionModeError(
                    f"Please Set Position Mode to `One-Way Mode` in Bybit App for {symbol}"
                )

            position = Position(
                symbol=symbol,
                exchange=self._exchange_id,
                side=side,
                signed_amount=signed_amount,
                entry_price=float(result.avgPrice),
                unrealized_pnl=float(result.unrealisedPnl),
                realized_pnl=float(result.cumRealisedPnl),
            )
            self._cache._apply_position(position)

    async def create_tp_sl_order(
        self,
        oid: str,
        symbol: str,
        side: OrderSide,
        type: OrderType,
        amount: Decimal,
        price: Decimal | None = None,
        time_in_force: TimeInForce | None = TimeInForce.GTC,
        tp_order_type: OrderType | None = None,
        tp_trigger_price: Decimal | None = None,
        tp_price: Decimal | None = None,
        tp_trigger_type: TriggerType | None = TriggerType.LAST_PRICE,
        sl_order_type: OrderType | None = None,
        sl_trigger_price: Decimal | None = None,
        sl_price: Decimal | None = None,
        sl_trigger_type: TriggerType | None = TriggerType.LAST_PRICE,
        **kwargs,
    ) -> Order:
        """Create a take profit and stop loss order"""
        market = self._market.get(symbol)
        if not market:
            raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
        id = market.id

        category = self._get_category(market)

        params = {
            "category": category,
            "symbol": id,
            "side": BybitEnumParser.to_bybit_order_side(side).value,
            "qty": str(amount),
            "orderLinkId": oid,
        }

        if type.is_limit:
            if not price:
                raise ValueError("Price is required for limit order")
            params["price"] = str(price)
            params["order_type"] = BybitOrderType.LIMIT.value
            params["timeInForce"] = BybitEnumParser.to_bybit_time_in_force(
                time_in_force
            ).value
        elif type.is_post_only:
            if not price:
                raise ValueError("Price is required for post-only order")
            params["order_type"] = BybitOrderType.LIMIT.value
            params["price"] = str(price)
            params["timeInForce"] = BybitTimeInForce.POST_ONLY.value
        elif type == OrderType.MARKET:
            params["order_type"] = BybitOrderType.MARKET.value

        if market.spot:
            params["marketUnit"] = "baseCoin"

        if tp_order_type:
            params["takeProfit"] = str(tp_trigger_price)
            params["triggerBy"] = BybitEnumParser.to_bybit_trigger_type(
                tp_trigger_type
            ).value
            if tp_order_type.is_limit:
                if not tp_price:
                    raise ValueError("Price is required for limit take profit order")
                params["tpOrderType"] = BybitOrderType.LIMIT.value
                params["tpLimitPrice"] = str(tp_price)

        if sl_order_type:
            params["stopLoss"] = str(sl_trigger_price)
            params["triggerBy"] = BybitEnumParser.to_bybit_trigger_type(
                sl_trigger_type
            ).value
            if sl_order_type.is_limit:
                if not sl_price:
                    raise ValueError("Price is required for limit stop loss order")
                params["slOrderType"] = BybitOrderType.LIMIT.value
                params["slLimitPrice"] = str(sl_price)

        if not market.spot:
            tpslMode = kwargs.pop("tpslMode", "Partial")
            params["tpslMode"] = tpslMode

        params.update(kwargs)

        try:
            res = await self._api_client.post_v5_order_create(**params)

            order = Order(
                exchange=self._exchange_id,
                eid=res.result.orderId,
                oid=oid,
                timestamp=int(res.time),
                symbol=symbol,
                type=type,
                side=side,
                amount=amount,
                price=float(price) if price else None,
                time_in_force=time_in_force,
                status=OrderStatus.PENDING,
                filled=Decimal(0),
                remaining=amount,
                reduce_only=False,
            )
            return order
        except Exception as e:
            error_msg = f"{e.__class__.__name__}: {str(e)}"
            self._log.error(f"Error creating order: {error_msg} params: {str(params)}")
            order = Order(
                exchange=self._exchange_id,
                timestamp=self._clock.timestamp_ms(),
                symbol=symbol,
                oid=oid,
                type=type,
                side=side,
                amount=amount,
                price=float(price) if price else None,
                time_in_force=time_in_force,
                status=OrderStatus.FAILED,
                filled=Decimal(0),
                remaining=amount,
                reason=error_msg,
            )
            return order

    async def create_batch_orders(self, orders: List[BatchOrderSubmit]):
        if not orders:
            raise ValueError("No orders provided for batch submission")

        # Get category from first order
        first_market = self._market.get(orders[0].symbol)
        if not first_market:
            raise ValueError(f"Symbol {orders[0].symbol} not found in market")
        category = self._get_category(first_market)

        batch_orders = []
        for order in orders:
            market = self._market.get(order.symbol)
            if not market:
                raise ValueError(f"Symbol {order.symbol} not found in market")
            id = market.id

            params = {
                "symbol": id,
                "side": BybitEnumParser.to_bybit_order_side(order.side).value,
                "qty": str(order.amount),
                "orderLinkId": order.oid,
            }
            if order.type.is_limit:
                if not order.price:
                    raise ValueError("Price is required for limit order")
                params["orderType"] = BybitOrderType.LIMIT.value
                params["price"] = str(order.price)
                params["timeInForce"] = BybitEnumParser.to_bybit_time_in_force(
                    order.time_in_force
                ).value
            elif order.type.is_post_only:
                if not order.price:
                    raise ValueError("Price is required for limit order")
                params["orderType"] = BybitOrderType.LIMIT.value
                params["price"] = str(order.price)
                params["timeInForce"] = BybitTimeInForce.POST_ONLY.value
            elif order.type == OrderType.MARKET:
                params["orderType"] = BybitOrderType.MARKET.value

            if order.reduce_only:
                params["reduceOnly"] = True
            if market.spot:
                params["marketUnit"] = "baseCoin"
            params.update(order.kwargs)
            batch_orders.append(params)

        try:
            res = await self._api_client.post_v5_order_create_batch(
                category=category, request=batch_orders
            )
            for order, res_order, res_ext in zip(
                orders, res.result.list, res.retExtInfo.list
            ):
                if res_ext.code == 0:
                    res_batch_order = Order(
                        exchange=self._exchange_id,
                        oid=order.oid,
                        eid=res_order.orderId,
                        timestamp=int(res_order.createAt),
                        symbol=order.symbol,
                        type=order.type,
                        side=order.side,
                        amount=order.amount,
                        price=float(order.price) if order.price else None,
                        time_in_force=order.time_in_force,
                        status=OrderStatus.PENDING,
                        filled=Decimal(0),
                        remaining=order.amount,
                        reduce_only=order.reduce_only,
                    )
                else:
                    res_batch_order = Order(
                        exchange=self._exchange_id,
                        timestamp=self._clock.timestamp_ms(),
                        symbol=order.symbol,
                        type=order.type,
                        oid=order.oid,
                        side=order.side,
                        amount=order.amount,
                        price=float(order.price) if order.price else None,
                        time_in_force=order.time_in_force,
                        status=OrderStatus.FAILED,
                        filled=Decimal(0),
                        remaining=order.amount,
                        reduce_only=order.reduce_only,
                        reason=res_ext.msg,
                    )
                    self._log.error(
                        f"Failed to place order for {order.symbol}: {res_ext.msg} code: {res_ext.code} {order.oid}"
                    )
                self.order_status_update(res_batch_order)
        except Exception as e:
            error_msg = f"{e.__class__.__name__}: {str(e)}"
            self._log.error(f"Error creating batch orders: {error_msg}")
            for order in orders:
                res_batch_order = Order(
                    exchange=self._exchange_id,
                    timestamp=self._clock.timestamp_ms(),
                    symbol=order.symbol,
                    oid=order.oid,
                    type=order.type,
                    side=order.side,
                    amount=order.amount,
                    price=float(order.price) if order.price else None,
                    time_in_force=order.time_in_force,
                    status=OrderStatus.FAILED,
                    filled=Decimal(0),
                    remaining=order.amount,
                    reduce_only=order.reduce_only,
                    reason=error_msg,
                )
                self.order_status_update(res_batch_order)

    async def create_order(
        self,
        oid: str,
        symbol: str,
        side: OrderSide,
        type: OrderType,
        amount: Decimal,
        price: Decimal | None = None,
        time_in_force: TimeInForce = TimeInForce.GTC,
        reduce_only: bool = False,
        **kwargs,
    ):
        market = self._market.get(symbol)
        if not market:
            raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
        id = market.id

        category = self._get_category(market)

        params = {
            "category": category,
            "symbol": id,
            "side": BybitEnumParser.to_bybit_order_side(side).value,
            "qty": str(amount),
            "orderLinkId": oid,
        }

        if type.is_limit:
            if not price:
                raise ValueError("Price is required for limit order")
            params["price"] = str(price)
            params["order_type"] = BybitOrderType.LIMIT.value
            params["timeInForce"] = BybitEnumParser.to_bybit_time_in_force(
                time_in_force
            ).value
        elif type.is_post_only:
            if not price:
                raise ValueError("Price is required for post-only order")
            params["order_type"] = BybitOrderType.LIMIT.value
            params["price"] = str(price)
            params["timeInForce"] = BybitTimeInForce.POST_ONLY.value
        elif type == OrderType.MARKET:
            params["order_type"] = BybitOrderType.MARKET.value

        # if position_side:
        #     params["positionIdx"] = BybitEnumParser.to_bybit_position_side(
        #         position_side
        #     ).value
        if reduce_only:
            params["reduceOnly"] = True
        if market.spot:
            params["marketUnit"] = "baseCoin"

        params.update(kwargs)

        try:
            res = await self._api_client.post_v5_order_create(**params)
            order = Order(
                exchange=self._exchange_id,
                eid=res.result.orderId,
                oid=oid,
                timestamp=int(res.time),
                symbol=symbol,
                type=type,
                side=side,
                amount=amount,
                price=float(price) if price else None,
                time_in_force=time_in_force,
                # position_side=position_side,
                status=OrderStatus.PENDING,
                filled=Decimal(0),
                remaining=amount,
                reduce_only=reduce_only,
            )
        except Exception as e:
            error_msg = f"{e.__class__.__name__}: {str(e)}"
            self._log.error(f"Error creating order: {error_msg} params: {str(params)}")
            order = Order(
                exchange=self._exchange_id,
                oid=oid,
                timestamp=self._clock.timestamp_ms(),
                symbol=symbol,
                type=type,
                side=side,
                amount=amount,
                price=float(price) if price else None,
                time_in_force=time_in_force,
                # position_side=position_side,
                status=OrderStatus.FAILED,
                filled=Decimal(0),
                remaining=amount,
                reduce_only=reduce_only,
                reason=error_msg,
            )
        self.order_status_update(order)

    async def create_order_ws(
        self,
        oid: str,
        symbol: str,
        side: OrderSide,
        type: OrderType,
        amount: Decimal,
        price: Decimal | None = None,
        time_in_force: TimeInForce = TimeInForce.GTC,
        reduce_only: bool = False,
        **kwargs,
    ):
        ws_fallback = kwargs.pop("ws_fallback", True)
        self._registry.register_tmp_order(
            order=Order(
                oid=oid,
                exchange=self._exchange_id,
                symbol=symbol,
                status=OrderStatus.INITIALIZED,
                amount=amount,
                type=type,
                side=side,
                price=float(price) if price else None,
                time_in_force=time_in_force,
                reduce_only=reduce_only,
                timestamp=self._clock.timestamp_ms(),
            )
        )
        market = self._market.get(symbol)
        if not market:
            raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
        id = market.id

        category = self._get_category(market)

        params = {
            "category": category,
            "symbol": id,
            "side": BybitEnumParser.to_bybit_order_side(side).value,
            "qty": str(amount),
            "orderLinkId": oid,
        }

        if type.is_limit:
            if not price:
                raise ValueError("Price is required for limit order")
            params["price"] = str(price)
            params["orderType"] = BybitOrderType.LIMIT.value
            params["timeInForce"] = BybitEnumParser.to_bybit_time_in_force(
                time_in_force
            ).value
        elif type.is_post_only:
            if not price:
                raise ValueError("Price is required for post-only order")
            params["orderType"] = BybitOrderType.LIMIT.value
            params["price"] = str(price)
            params["timeInForce"] = BybitTimeInForce.POST_ONLY.value
        elif type == OrderType.MARKET:
            params["orderType"] = BybitOrderType.MARKET.value

        # if position_side:
        #     params["positionIdx"] = BybitEnumParser.to_bybit_position_side(
        #         position_side
        #     ).value
        if reduce_only:
            params["reduceOnly"] = True
        if market.spot:
            params["marketUnit"] = "baseCoin"

        params.update(kwargs)

        ack_future: asyncio.Future = asyncio.get_event_loop().create_future()
        self._pending_ws_acks[oid] = ack_future
        try:
            await self._ws_api_client.create_order(id=oid, **params)
        except WsRequestNotSentError as e:
            self._pending_ws_acks.pop(oid, None)
            self._log.warning(f"[{symbol}] create_order_ws send failed: {e}")
            if ws_fallback:
                await self.create_order(
                    oid=oid,
                    symbol=symbol,
                    side=side,
                    type=type,
                    amount=amount,
                    price=price,
                    time_in_force=time_in_force,
                    reduce_only=reduce_only,
                    **kwargs,
                )
                return
            else:
                self.order_status_update(
                    Order(
                        exchange=self._exchange_id,
                        oid=oid,
                        timestamp=self._clock.timestamp_ms(),
                        symbol=symbol,
                        type=type,
                        side=side,
                        amount=amount,
                        price=float(price) if price else None,
                        time_in_force=time_in_force,
                        status=OrderStatus.FAILED,
                        filled=Decimal(0),
                        remaining=amount,
                        reduce_only=reduce_only,
                        reason=f"WS_REQUEST_NOT_SENT: {e}",
                    )
                )
                raise

        try:
            await asyncio.wait_for(asyncio.shield(ack_future), timeout=5.0)
        except (asyncio.TimeoutError, WsRequestNotSentError):
            self._pending_ws_acks.pop(oid, None)
            self._log.warning(
                f"[{symbol}] create_order_ws ACK not received for oid={oid}, confirming via REST..."
            )
            if not await self._confirm_order_after_ack_timeout(oid, symbol):
                raise WsAckTimeoutError(oid=oid, timeout=5.0)
            return WsOrderResultType.ACK_TIMEOUT_CONFIRMED

    async def cancel_all_orders(self, symbol: str) -> bool:
        try:
            market = self._market.get(symbol)
            if not market:
                raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
            symbol = market.id

            category = self._get_category(market)

            params = {
                "category": category,
                "symbol": symbol,
            }

            await self._api_client.post_v5_order_cancel_all(**params)
            return True
        except Exception as e:
            error_msg = f"{e.__class__.__name__}: {str(e)}"
            self._log.error(
                f"Error canceling all orders: {error_msg} params: {str(params)}"
            )
            return False

    async def modify_order(
        self,
        oid: str,
        symbol: str,
        side: OrderSide | None = None,
        price: Decimal | None = None,
        amount: Decimal | None = None,
        **kwargs,
    ):
        # NOTE: side is not supported for modify order
        market = self._market.get(symbol)
        if not market:
            raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
        id = market.id

        category = self._get_category(market)
        params = {
            "category": category,
            "symbol": id,
            "orderLinkId": oid,
            "price": str(price) if price else None,
            "qty": str(amount) if amount else None,
            **kwargs,
        }

        try:
            res = await self._api_client.post_v5_order_amend(**params)
            order = Order(
                exchange=self._exchange_id,
                eid=res.result.orderId,
                oid=oid,
                timestamp=int(res.time),
                symbol=symbol,
                status=OrderStatus.PENDING,
                filled=Decimal(0),
                price=float(price) if price else None,
                remaining=amount,
            )
            return order
        except Exception as e:
            error_msg = f"{e.__class__.__name__}: {str(e)}"
            self._log.error(f"Error modifying order: {error_msg} params: {str(params)}")
            order = Order(
                exchange=self._exchange_id,
                timestamp=self._clock.timestamp_ms(),
                symbol=symbol,
                oid=oid,
                status=OrderStatus.FAILED,
                filled=Decimal(0),
                remaining=amount,
                price=float(price) if price else None,
                reason=error_msg,
            )
            return order

    def _parse_order_update(self, raw: bytes):
        order_msg = self._ws_msg_order_update_decoder.decode(raw)
        self._log.debug(f"Order update: {str(order_msg)}")
        for data in order_msg.data:
            category = data.category
            if category.is_spot:
                id = data.symbol + "_spot"
            elif category.is_linear:
                id = data.symbol + "_linear"
            elif category.is_inverse:
                id = data.symbol + "_inverse"
            symbol = self._market_id[id]

            order = Order(
                exchange=self._exchange_id,
                symbol=symbol,
                status=BybitEnumParser.parse_order_status(data.orderStatus),
                eid=data.orderId,
                oid=data.orderLinkId,
                timestamp=int(data.updatedTime),
                type=BybitEnumParser.parse_order_type(data.orderType, data.timeInForce),
                side=BybitEnumParser.parse_order_side(data.side),
                time_in_force=BybitEnumParser.parse_time_in_force(data.timeInForce),
                price=float(data.price),
                average=float(data.avgPrice) if data.avgPrice else None,
                amount=Decimal(data.qty),
                filled=Decimal(data.cumExecQty),
                remaining=Decimal(data.qty) - Decimal(data.cumExecQty),
                fee=Decimal(data.cumExecFee),
                fee_currency=data.feeCurrency,
                cum_cost=Decimal(data.cumExecValue),
                reduce_only=data.reduceOnly,
                position_side=BybitEnumParser.parse_position_side(data.positionIdx),
            )

            self.order_status_update(order)

    async def fetch_order(
        self, symbol: str, oid: str, force_refresh: bool = False
    ) -> Order | None:
        if not force_refresh:
            cached = self._cache.get_order(oid)
            if cached is not None and isinstance(cached, Order):
                return cached
        market = self._market.get(symbol)
        if not market:
            return None
        category = self._get_category(market)
        try:
            res = await self._api_client.get_v5_order_realtime(
                category=category, symbol=market.id, orderLinkId=oid, limit=50
            )
            for item in res.result.list:
                order = self._convert_rest_order(item, res.result.category)
                if order is not None and order.oid == oid:
                    return order
        except Exception as e:
            self._log.error(f"fetch_order failed for {symbol}#{oid}: {e}")
        return None

    async def fetch_open_orders(self, symbol: str) -> list[Order]:
        market = self._market.get(symbol)
        if not market:
            return []
        category = self._get_category(market)
        try:
            all_orders: list[Order] = []
            cursor = ""
            while True:
                res = await self._api_client.get_v5_order_realtime(
                    category=category, symbol=market.id, limit=50, cursor=cursor
                )
                for item in res.result.list:
                    order = self._convert_rest_order(item, res.result.category)
                    if order is not None:
                        all_orders.append(order)
                if not res.result.nextPageCursor:
                    break
                cursor = res.result.nextPageCursor
            return all_orders
        except Exception as e:
            self._log.error(f"fetch_open_orders failed for {symbol}: {e}")
            # fallback to cache snapshot
            oids = self._cache.get_open_orders(symbol)
            orders: list[Order] = []
            for oid in oids:
                order = self._cache.get_order(oid)
                if order is not None and isinstance(order, Order):
                    orders.append(order)
            return orders

    async def _resync_after_reconnect(self):
        before_positions = set(self._cache.get_all_positions(self._exchange_id).keys())
        before_open_orders = set(
            self._cache.get_open_orders(
                exchange=self._exchange_id, include_canceling=True
            )
        )
        try:
            await self._async_resync_init()

            candidate_symbols = set(before_positions)
            for oid in before_open_orders:
                cached = self._cache.get_order(oid)
                if cached is not None and isinstance(cached, Order) and cached.symbol:
                    candidate_symbols.add(cached.symbol)

            fetched_open_oids: set[str] = set()
            for symbol in candidate_symbols:
                for order in await self.fetch_open_orders(symbol):
                    fetched_open_oids.add(order.oid)
                    cached = self._cache.get_order(order.oid)
                    silent = (
                        cached is not None
                        and isinstance(cached, Order)
                        and cached.status == order.status
                    )
                    self.order_status_update(order, silent=silent)

            # Conservative: only close missing orders after grace + explicit fetch_order confirmation.
            missing_oids = before_open_orders - fetched_open_oids
            await self._confirm_missing_open_orders(missing_oids)

            # Pull recent trades: check registered orders not in before_open_orders
            # (e.g. PENDING orders that may have filled during disconnect).
            for symbol in candidate_symbols:
                for oid in self._cache.get_symbol_orders(symbol):
                    if not self._registry.is_registered(oid):
                        continue
                    cached = self._cache.get_order(oid)
                    if cached is None or not isinstance(cached, Order):
                        continue
                    if cached.is_closed or oid in before_open_orders:
                        continue
                    try:
                        latest = await self.fetch_order(symbol, oid, force_refresh=True)
                        if latest is not None:
                            self.order_status_update(latest)
                    except Exception as e:
                        self._log.warning(
                            f"Bybit reconnect recent-trade check failed for {symbol}#{oid}: {e}"
                        )

            after_positions = set(
                self._cache.get_all_positions(self._exchange_id).keys()
            )
            after_open_orders = set(
                self._cache.get_open_orders(
                    exchange=self._exchange_id, include_canceling=True
                )
            )
            return {
                "success": True,
                "positions_opened": sorted(after_positions - before_positions),
                "positions_closed": sorted(before_positions - after_positions),
                "open_orders_added": sorted(after_open_orders - before_open_orders),
                "open_orders_removed": sorted(before_open_orders - after_open_orders),
            }
        except Exception as e:
            self._log.error(f"Bybit reconnect resync failed: {e}")
            return {
                "success": False,
                "error": str(e),
                "positions_opened": [],
                "positions_closed": [],
                "open_orders_added": [],
                "open_orders_removed": [],
            }

    def _parse_position_update(self, raw: bytes):
        position_msg = self._ws_msg_position_decoder.decode(raw)
        self._log.debug(f"Position update: {str(position_msg)}")

        for data in position_msg.data:
            category = data.category
            if category.is_linear:  # only linear/inverse/ position is supported
                id = data.symbol + "_linear"
            elif category.is_inverse:
                id = data.symbol + "_inverse"
            symbol = self._market_id[id]

            side = data.side.parse_to_position_side()
            if side == PositionSide.LONG:
                signed_amount = Decimal(data.size)
            elif side == PositionSide.SHORT:
                signed_amount = -Decimal(data.size)
            else:
                side = None
                signed_amount = Decimal(0)

            position = Position(
                symbol=symbol,
                exchange=self._exchange_id,
                side=side,
                signed_amount=signed_amount,
                entry_price=float(data.entryPrice),
                unrealized_pnl=float(data.unrealisedPnl),
                realized_pnl=float(data.cumRealisedPnl),
            )

            self._cache._apply_position(position)

    def _parse_wallet_update(self, raw: bytes):
        wallet_msg = self._ws_msg_wallet_decoder.decode(raw)
        self._log.debug(f"Wallet update: {str(wallet_msg)}")

        for data in wallet_msg.data:
            balances = data.parse_to_balances()
            self._cache._apply_balance(self._account_type, balances)
