import asyncio
import msgspec
import warnings
from typing import Dict, List
from decimal import Decimal
from nexustrader.error import (
    PositionModeError,
    WsRequestNotSentError,
    WsAckTimeoutError,
    WsAckRejectedError,
)
from nexustrader.constants import WsOrderResultType
from nexustrader.exchange.okx import OkxAccountType
from nexustrader.exchange.okx.websockets import OkxWSClient, OkxWSApiClient
from nexustrader.exchange.okx.schema import OkxWsGeneralMsg
from nexustrader.schema import (
    Order,
    Position,
    BatchOrderSubmit,
)
from nexustrader.exchange.okx.schema import (
    OkxMarket,
    OkxWsOrderMsg,
    OkxWsPositionMsg,
    OkxWsAccountMsg,
    OkxBalanceResponse,
    OkxOrderResponse,
    OkxPositionResponse,
    OkxWsApiOrderResponse,
)
from nexustrader.constants import (
    OrderStatus,
    TimeInForce,
    ExchangeType,
    PositionSide,
    TriggerType,
)
from nexustrader.core.nautilius_core import LiveClock, MessageBus
from nexustrader.core.cache import AsyncCache
from nexustrader.core.entity import TaskManager
from nexustrader.exchange.okx.rest_api import OkxApiClient
from nexustrader.constants import OrderSide, OrderType
from nexustrader.exchange.okx.constants import (
    OkxTdMode,
    OkxEnumParser,
)
from nexustrader.base import OrderManagementSystem
from nexustrader.core.registry import OrderRegistry


class OkxOrderManagementSystem(OrderManagementSystem):
    _ws_client: OkxWSClient
    _account_type: OkxAccountType
    _market: Dict[str, OkxMarket]
    _market_id: Dict[str, str]
    _api_client: OkxApiClient

    def __init__(
        self,
        account_type: OkxAccountType,
        api_key: str,
        secret: str,
        passphrase: str,
        market: Dict[str, OkxMarket],
        market_id: Dict[str, str],
        registry: OrderRegistry,
        cache: AsyncCache,
        api_client: OkxApiClient,
        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=OkxWSClient(
                account_type=account_type,
                handler=self._ws_msg_handler,
                clock=clock,
                task_manager=task_manager,
                api_key=api_key,
                secret=secret,
                passphrase=passphrase,
            ),
            exchange_id=exchange_id,
            clock=clock,
            msgbus=msgbus,
            task_manager=task_manager,
        )

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

        self._pending_ws_acks: Dict[str, asyncio.Future] = {}

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

        self._decoder_ws_general_msg = msgspec.json.Decoder(OkxWsGeneralMsg)
        self._decoder_ws_order_msg = msgspec.json.Decoder(OkxWsOrderMsg, strict=False)
        self._decoder_ws_position_msg = msgspec.json.Decoder(
            OkxWsPositionMsg, strict=False
        )
        self._decoder_ws_account_msg = msgspec.json.Decoder(
            OkxWsAccountMsg, strict=False
        )
        self._ws_msg_ws_api_response_decoder = msgspec.json.Decoder(
            OkxWsApiOrderResponse
        )

    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 _ws_api_msg_handler(self, raw: bytes):
        # if raw == b"pong":
        #     self._ws_api_client._transport.notify_user_specific_pong_received()
        #     self._log.debug(f"Pong received: {str(raw)}")
        #     return
        try:
            ws_msg: OkxWsGeneralMsg = self._decoder_ws_general_msg.decode(raw)
            if ws_msg.is_event_msg:
                self._handle_event_msg(ws_msg)
                if ws_msg.event == "login":
                    self._ws_api_client.notify_auth_success()
            else:
                msg = self._ws_msg_ws_api_response_decoder.decode(raw)
                oid = msg.id

                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
                side = tmp_order.side
                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.op.is_place_order:
                    if msg.is_success:
                        ordId = msg.data[0].ordId
                        self._log.debug(
                            f"[{symbol}] new order success: oid: {oid} eid: {ordId}"
                        )
                        order = Order(
                            exchange=self._exchange_id,
                            symbol=symbol,
                            oid=oid,
                            eid=ordId,
                            status=OrderStatus.PENDING,
                            amount=amount,
                            type=type,
                            side=side,
                            price=price,
                            time_in_force=time_in_force,
                            reduce_only=reduce_only,
                            timestamp=ts,
                        )
                        self.order_status_update(order)
                        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,
                            side=side,
                            type=type,
                            price=price,
                            timestamp=ts,
                            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)
                elif msg.op.is_cancel_order:
                    if msg.is_success:
                        ordId = msg.data[0].ordId
                        self._log.debug(
                            f"[{symbol}] canceling order success: oid: {oid} eid: {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,
                            timestamp=ts,
                            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)
        except msgspec.DecodeError as e:
            self._log.error(f"Error decoding WebSocket API message: {str(raw)} {e}")

    def _order_data_to_order(self, symbol: str, data) -> Order:
        market = self._market[symbol]
        if not market.spot:
            ct_val = Decimal(market.info.ctVal)
        else:
            ct_val = Decimal("1")
        return Order(
            exchange=self._exchange_id,
            symbol=symbol,
            status=OkxEnumParser.parse_order_status(data.state),
            eid=data.ordId,
            amount=Decimal(data.sz) * ct_val,
            filled=Decimal(data.accFillSz) * ct_val,
            oid=data.clOrdId,
            timestamp=int(data.uTime),
            type=OkxEnumParser.parse_order_type(data.ordType),
            side=OkxEnumParser.parse_order_side(data.side),
            time_in_force=OkxEnumParser.parse_time_in_force(data.ordType),
            price=float(data.px) if data.px else None,
            average=float(data.avgPx) if data.avgPx else None,
            last_filled_price=float(data.fillPx) if data.fillPx else None,
            last_filled=Decimal(data.fillSz) * ct_val if data.fillSz else Decimal(0),
            remaining=Decimal(data.sz) * ct_val - Decimal(data.accFillSz) * ct_val,
            fee=Decimal(data.fee),
            fee_currency=data.feeCcy,
            cost=Decimal(data.avgPx) * Decimal(data.fillSz) * ct_val,
            cum_cost=Decimal(data.avgPx) * Decimal(data.accFillSz) * ct_val,
            reduce_only=data.reduceOnly,
            position_side=OkxEnumParser.parse_position_side(data.posSide),
        )

    def _position_mode_check(self):
        res = self._run_sync(self._api_client.get_api_v5_account_config())
        for data in res.data:
            if not data.posMode.is_one_way_mode:
                raise PositionModeError(
                    "Please Set Position Mode to `One-Way Mode` in OKX App"
                )
            if data.acctLv.is_portfolio_margin:
                warnings.warn(
                    "For Portfolio Margin Account, `Reduce Only` is not supported"
                )
            self._acctLv = data.acctLv

    def _init_account_balance(self):
        res: OkxBalanceResponse = self._run_sync(
            self._api_client.get_api_v5_account_balance()
        )
        for data in res.data:
            self._cache._apply_balance(self._account_type, data.parse_to_balances())

    def _init_position(self):
        res: OkxPositionResponse = self._run_sync(
            self._api_client.get_api_v5_account_positions()
        )
        for data in res.data:
            side = data.posSide.parse_to_position_side()
            if side == PositionSide.FLAT:
                signed_amount = Decimal(data.pos)
                if signed_amount > 0:
                    side = PositionSide.LONG
                elif signed_amount < 0:
                    side = PositionSide.SHORT
                else:
                    side = None
            elif side == PositionSide.LONG:
                signed_amount = Decimal(data.pos)
            elif side == PositionSide.SHORT:
                signed_amount = -Decimal(data.pos)

            symbol = self._market_id.get(data.instId)
            if not symbol:
                warnings.warn(f"Symbol {data.instId} not found in market")
                continue

            market = self._market[symbol]

            if market.info.ctVal:
                ct_val = Decimal(market.info.ctVal)
            else:
                ct_val = Decimal("1")

            position = Position(
                symbol=symbol,
                exchange=self._exchange_id,
                side=side,
                signed_amount=signed_amount * ct_val,
                entry_price=float(data.avgPx) if data.avgPx else 0,
                unrealized_pnl=float(data.upl) if data.upl else 0,
                realized_pnl=float(data.realizedPnl) if data.realizedPnl else 0,
            )
            self._cache._apply_position(position)

    def _handle_event_msg(self, msg: OkxWsGeneralMsg):
        if msg.event == "error":
            self._log.error(msg.error_msg)
        elif msg.event == "login":
            self._log.debug(msg.login_msg)
        elif msg.event == "subscribe":
            self._log.debug(f"Subscribed to {msg.subscribe_msg}")

    def _ws_msg_handler(self, raw: bytes):
        # if raw == b"pong":
        #     self._ws_client._transport.notify_user_specific_pong_received()
        #     self._log.debug(f"Pong received: {str(raw)}")
        #     return
        try:
            ws_msg: OkxWsGeneralMsg = self._decoder_ws_general_msg.decode(raw)
            if ws_msg.is_event_msg:
                self._handle_event_msg(ws_msg)
                if ws_msg.event == "login":
                    self._ws_client.notify_auth_success()
            else:
                channel = ws_msg.arg.channel
                if channel == "orders":
                    self._handle_orders(raw)
                elif channel == "positions":
                    self._handle_positions(raw)
                elif channel == "account":
                    self._handle_account(raw)
        except msgspec.DecodeError as e:
            self._log.error(f"Error decoding message: {str(raw)} {e}")

    def _handle_orders(self, raw: bytes):
        msg: OkxWsOrderMsg = self._decoder_ws_order_msg.decode(raw)
        self._log.debug(f"Order update: {str(msg)}")
        for data in msg.data:
            symbol = self._market_id[data.instId]

            market = self._market[symbol]

            if not market.spot:
                ct_val = Decimal(market.info.ctVal)  # contract size
            else:
                ct_val = Decimal("1")

            order = Order(
                exchange=self._exchange_id,
                symbol=symbol,
                status=OkxEnumParser.parse_order_status(data.state),
                eid=data.ordId,
                amount=Decimal(data.sz) * ct_val,
                filled=Decimal(data.accFillSz) * ct_val,
                oid=data.clOrdId,
                timestamp=data.uTime,
                type=OkxEnumParser.parse_order_type(data.ordType),
                side=OkxEnumParser.parse_order_side(data.side),
                time_in_force=OkxEnumParser.parse_time_in_force(data.ordType),
                price=float(data.px) if data.px else None,
                average=float(data.avgPx) if data.avgPx else None,
                last_filled_price=float(data.fillPx) if data.fillPx else None,
                last_filled=Decimal(data.fillSz) * ct_val
                if data.fillSz
                else Decimal(0),
                remaining=Decimal(data.sz) * ct_val - Decimal(data.accFillSz) * ct_val,
                fee=Decimal(data.fee),  # accumalated fee
                fee_currency=data.feeCcy,  # accumalated fee currency
                cost=Decimal(data.avgPx) * Decimal(data.fillSz) * ct_val,
                cum_cost=Decimal(data.avgPx) * Decimal(data.accFillSz) * ct_val,
                reduce_only=data.reduceOnly,
                position_side=OkxEnumParser.parse_position_side(data.posSide),
            )
            self.order_status_update(order)

    def _handle_positions(self, raw: bytes):
        position_msg = self._decoder_ws_position_msg.decode(raw)
        self._log.debug(f"Okx Position Msg: {str(position_msg)}")

        for data in position_msg.data:
            symbol = self._market_id.get(data.instId)
            if not symbol:
                continue
            market = self._market[symbol]

            if market.info.ctVal:
                ct_val = Decimal(market.info.ctVal)
            else:
                ct_val = Decimal("1")

            side = data.posSide.parse_to_position_side()
            if side == PositionSide.LONG:
                signed_amount = Decimal(data.pos)
            elif side == PositionSide.SHORT:
                signed_amount = -Decimal(data.pos)
            elif side == PositionSide.FLAT:
                # one way mode, posSide always is 'net' from OKX ws msg, and pos amount is signed
                signed_amount = Decimal(data.pos)
                if signed_amount > 0:
                    side = PositionSide.LONG
                elif signed_amount < 0:
                    side = PositionSide.SHORT
                else:
                    side = None
            else:
                self._log.warning(f"Invalid position side: {side}")

            position = Position(
                symbol=symbol,
                exchange=self._exchange_id,
                side=side,
                signed_amount=signed_amount * ct_val,
                entry_price=float(data.avgPx) if data.avgPx else 0,
                unrealized_pnl=float(data.upl) if data.upl else 0,
                realized_pnl=float(data.realizedPnl) if data.realizedPnl else 0,
            )
            self._log.debug(f"Position updated: {str(position)}")
            self._cache._apply_position(position)

    def _handle_account(self, raw: bytes):
        account_msg: OkxWsAccountMsg = self._decoder_ws_account_msg.decode(raw)
        self._log.debug(f"Account update: {str(account_msg)}")

        for data in account_msg.data:
            balances = data.parse_to_balance()
            self._cache._apply_balance(self._account_type, balances)

    def _get_td_mode(self, market: OkxMarket):
        if (
            not market.spot
            or self._acctLv.is_portfolio_margin
            or self._acctLv.is_multi_currency_margin
        ):
            return OkxTdMode.CROSS
        else:
            return OkxTdMode.CASH

    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")
        inst_id = market.id

        td_mode = kwargs.pop("td_mode", None) or kwargs.pop("tdMode", None)
        if not td_mode:
            td_mode = self._get_td_mode(market)
        else:
            td_mode = OkxTdMode(td_mode)

        if not market.spot:
            ct_val = Decimal(market.info.ctVal)  # contract size
            sz = format(amount / ct_val, "f")
        else:
            sz = str(amount)

        params = {
            "inst_id": inst_id,
            "td_mode": td_mode.value,
            "side": OkxEnumParser.to_okx_order_side(side).value,
            "ord_type": OkxEnumParser.to_okx_order_type(type, time_in_force).value,
            "sz": sz,
            "clOrdId": oid,
        }

        if type.is_limit or type.is_post_only:
            if not price:
                raise ValueError("Price is required for limit order")
            params["px"] = str(price)
        else:
            if market.spot and not self._acctLv.is_futures and not td_mode.is_isolated:
                params["tgtCcy"] = "base_ccy"

        if (
            market.spot
            and self._acctLv.is_futures
            and (td_mode.is_cross or td_mode.is_isolated)
        ):
            if side == OrderSide.BUY:
                params["ccy"] = market.quote
            else:
                params["ccy"] = market.base

        attachAlgoOrds = {}
        if tp_trigger_price is not None:
            attachAlgoOrds["tpTriggerPx"] = str(tp_trigger_price)
            attachAlgoOrds["tpTriggerPxType"] = OkxEnumParser.to_okx_trigger_type(
                tp_trigger_type
            ).value
            if tp_order_type.is_limit:
                attachAlgoOrds["tpOrdPx"] = str(tp_price)
            else:
                attachAlgoOrds["tpOrdPx"] = "-1"

        if sl_trigger_price is not None:
            attachAlgoOrds["slTriggerPx"] = str(sl_trigger_price)
            attachAlgoOrds["slTriggerPxType"] = OkxEnumParser.to_okx_trigger_type(
                sl_trigger_type
            ).value
            if sl_order_type.is_limit:
                attachAlgoOrds["slOrdPx"] = str(sl_price)
            else:
                attachAlgoOrds["slOrdPx"] = "-1"

        if attachAlgoOrds:
            params["attachAlgoOrds"] = attachAlgoOrds

        params.update(kwargs)

        try:
            res = await self._api_client.post_api_v5_trade_order(**params)
            res = res.data[0]

            order = Order(
                exchange=self._exchange_id,
                eid=res.ordId,
                oid=oid,
                timestamp=int(res.ts),
                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,
            )
            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("Orders list cannot be empty")

        batch_orders = []
        for order in orders:
            market = self._market.get(order.symbol)
            if not market:
                raise ValueError(
                    f"Symbol {order.symbol} formated wrongly, or not supported"
                )
            inst_id = market.id

            td_mode = order.kwargs.pop("td_mode", None) or order.kwargs.pop(
                "tdMode", None
            )
            if not td_mode:
                td_mode = self._get_td_mode(market)
            else:
                td_mode = OkxTdMode(td_mode)

            if not market.spot:
                ct_val = Decimal(market.info.ctVal)
                sz = format(order.amount / ct_val, "f")
            else:
                sz = str(order.amount)

            params = {
                "inst_id": inst_id,
                "td_mode": td_mode.value,
                "side": OkxEnumParser.to_okx_order_side(order.side).value,
                "ord_type": OkxEnumParser.to_okx_order_type(
                    order.type, order.time_in_force
                ).value,
                "sz": sz,
                "clOrdId": order.oid,
            }

            if order.type.is_limit or order.type.is_post_only:
                if not order.price:
                    raise ValueError("Price is required for limit order")
                params["px"] = str(order.price)
            else:
                if (
                    market.spot
                    and not self._acctLv.is_futures
                    and not td_mode.is_isolated
                ):
                    params["tgtCcy"] = "base_ccy"

            if (
                market.spot
                and self._acctLv.is_futures
                and (td_mode.is_cross or td_mode.is_isolated)
            ):
                if order.side == OrderSide.BUY:
                    params["ccy"] = market.quote
                else:
                    params["ccy"] = market.base

            params["reduceOnly"] = order.reduce_only

            params.update(order.kwargs)
            batch_orders.append(params)

        try:
            res = await self._api_client.post_api_v5_trade_batch_orders(
                payload=batch_orders
            )
            for order, res_order in zip(orders, res.data):
                if res_order.sCode == "0":
                    order_result = Order(
                        exchange=self._exchange_id,
                        oid=order.oid,
                        eid=res_order.ordId,
                        timestamp=int(res_order.ts),
                        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:
                    order_result = Order(
                        exchange=self._exchange_id,
                        oid=order.oid,
                        timestamp=self._clock.timestamp_ms(),
                        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.FAILED,
                        filled=Decimal(0),
                        remaining=order.amount,
                        reduce_only=order.reduce_only,
                        reason=res_order.sMsg,
                    )
                    self._log.error(
                        f"Failed to create order for {order.symbol}: {res_order.sMsg}: {res_order.sCode}: {order.oid}"
                    )
                self.order_status_update(order_result)
        except Exception as e:
            error_msg = f"{e.__class__.__name__}: {str(e)}"
            self._log.error(
                f"Error creating batch orders: {error_msg} params: {str(orders)}"
            )
            for order in orders:
                order_result = 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,
                    reason=error_msg,
                )
                self.order_status_update(order_result)

    async def create_order_ws(
        self,
        oid: str,
        symbol: str,
        side: OrderSide,
        type: OrderType,
        amount: Decimal,
        price: Decimal = 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,
                price=float(price) if price else None,
                time_in_force=time_in_force,
                timestamp=self._clock.timestamp_ms(),
                reduce_only=reduce_only,
            )
        )
        market = self._market.get(symbol)
        if not market:
            raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
        inst_id = market.id
        inst_id_code = market.info.instIdCode

        td_mode = kwargs.pop("td_mode", None) or kwargs.pop("tdMode", None)
        if not td_mode:
            td_mode = self._get_td_mode(market)
        else:
            td_mode = OkxTdMode(td_mode)

        if not market.spot:
            ct_val = Decimal(market.info.ctVal)  # contract size
            sz = format(amount / ct_val, "f")
        else:
            sz = str(amount)

        params = {
            "tdMode": td_mode.value,
            "side": OkxEnumParser.to_okx_order_side(side).value,
            "ordType": OkxEnumParser.to_okx_order_type(type, time_in_force).value,
            "sz": sz,
            "clOrdId": oid,
        }

        if inst_id_code:
            params["instIdCode"] = inst_id_code
        else:
            params["instId"] = inst_id

        if type.is_limit or type.is_post_only:
            if not price:
                raise ValueError("Price is required for limit order")
            params["px"] = str(price)
        else:
            if market.spot and not self._acctLv.is_futures and not td_mode.is_isolated:
                params["tgtCcy"] = "base_ccy"

        if (
            market.spot
            and self._acctLv.is_futures
            and (td_mode.is_cross or td_mode.is_isolated)
        ):
            if side == OrderSide.BUY:
                params["ccy"] = market.quote
            else:
                params["ccy"] = market.base

        params["reduceOnly"] = reduce_only

        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.place_order(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(
                        oid=oid,
                        exchange=self._exchange_id,
                        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,
                        reduce_only=reduce_only,
                        status=OrderStatus.FAILED,
                        filled=Decimal(0),
                        remaining=amount,
                        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 create_order(
        self,
        oid: str,
        symbol: str,
        side: OrderSide,
        type: OrderType,
        amount: Decimal,
        price: Decimal = 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")
        inst_id = market.id

        td_mode = kwargs.pop("td_mode", None) or kwargs.pop("tdMode", None)
        if not td_mode:
            td_mode = self._get_td_mode(market)
        else:
            td_mode = OkxTdMode(td_mode)

        if not market.spot:
            ct_val = Decimal(market.info.ctVal)  # contract size
            sz = format(amount / ct_val, "f")
        else:
            sz = str(amount)

        params = {
            "inst_id": inst_id,
            "td_mode": td_mode.value,
            "side": OkxEnumParser.to_okx_order_side(side).value,
            "ord_type": OkxEnumParser.to_okx_order_type(type, time_in_force).value,
            "sz": sz,
            "clOrdId": oid,
        }

        if type.is_limit or type.is_post_only:
            if not price:
                raise ValueError("Price is required for limit order")
            params["px"] = str(price)
        else:
            if market.spot and not self._acctLv.is_futures and not td_mode.is_isolated:
                params["tgtCcy"] = "base_ccy"

        if (
            market.spot
            and self._acctLv.is_futures
            and (td_mode.is_cross or td_mode.is_isolated)
        ):
            if side == OrderSide.BUY:
                params["ccy"] = market.quote
            else:
                params["ccy"] = market.base

        # if position_side:
        #     params["posSide"] = OkxEnumParser.to_okx_position_side(position_side).value

        params["reduceOnly"] = reduce_only

        params.update(kwargs)

        try:
            res = await self._api_client.post_api_v5_trade_order(**params)
            res = res.data[0]
            order = Order(
                oid=oid,
                eid=res.ordId,
                exchange=self._exchange_id,
                timestamp=int(res.ts),
                symbol=symbol,
                type=type,
                side=side,
                amount=amount,
                price=float(price) if price else None,
                time_in_force=time_in_force,
                reduce_only=reduce_only,
                status=OrderStatus.PENDING,
                filled=Decimal(0),
                remaining=amount,
            )
        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(
                oid=oid,
                exchange=self._exchange_id,
                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,
                reduce_only=reduce_only,
                status=OrderStatus.FAILED,
                filled=Decimal(0),
                remaining=amount,
                reason=error_msg,
            )
        self.order_status_update(order)

    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")
        inst_id = market.id
        inst_id_code = market.info.instIdCode

        params = {"clOrdId": oid}
        if inst_id_code:
            params["instIdCode"] = inst_id_code
        else:
            params["instId"] = inst_id

        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(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(
                        exchange=self._exchange_id,
                        oid=oid,
                        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")
        inst_id = market.id

        params = {"instId": inst_id, "clOrdId": oid}
        params.update(kwargs)

        try:
            res = await self._api_client.post_api_v5_trade_cancel_order(**params)
            res = res.data[0]
            order = Order(
                exchange=self._exchange_id,
                eid=res.ordId,
                oid=oid,
                timestamp=int(res.ts),
                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)

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

        if not market.spot:
            ct_val = Decimal(market.info.ctVal)  # contract size
            sz = format(amount / ct_val, "f") if amount else None
        else:
            sz = str(amount) if amount else None

        params = {
            "instId": inst_id,
            "newPx": str(price) if price else None,
            "newSz": sz,
            "clOrdId": oid,
            **kwargs,
        }

        try:
            res = await self._api_client.post_api_v5_trade_amend_order(**params)
            res = res.data[0]
            order = Order(
                exchange=self._exchange_id,
                eid=res.ordId,
                oid=oid,
                timestamp=int(res.ts),
                symbol=symbol,
                status=OrderStatus.PENDING,
            )
        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,
                oid=oid,
                timestamp=self._clock.timestamp_ms(),
                symbol=symbol,
                status=OrderStatus.FAILED,
                reason=error_msg,
            )
        self.order_status_update(order)

    async def get_order(self, symbol: str, order_id: str):
        market = self._market.get(symbol)
        if not market:
            return None
        try:
            res: OkxOrderResponse = await self._api_client.get_api_v5_trade_order(
                inst_id=market.id, ord_id=order_id
            )
            if not res.data:
                return None

            data = res.data[0]
            if not market.spot:
                ct_val = Decimal(market.info.ctVal)  # type: ignore
            else:
                ct_val = Decimal("1")
            order = Order(
                exchange=self._exchange_id,
                symbol=symbol,
                status=OkxEnumParser.parse_order_status(data.state),
                id=data.ordId,
                amount=Decimal(data.sz) * ct_val,
                filled=Decimal(data.accFillSz) * ct_val,
                client_order_id=data.clOrdId,
                timestamp=int(data.uTime),
                type=OkxEnumParser.parse_order_type(data.ordType),
                side=OkxEnumParser.parse_order_side(data.side),
                time_in_force=OkxEnumParser.parse_time_in_force(data.ordType),
                price=float(data.px) if data.px else None,
                average=float(data.avgPx) if data.avgPx else None,
                last_filled_price=float(data.fillPx) if data.fillPx else None,
                last_filled=Decimal(data.fillSz) * ct_val
                if data.fillSz
                else Decimal(0),
                remaining=Decimal(data.sz) * ct_val - Decimal(data.accFillSz) * ct_val,
                fee=Decimal(data.fee),  # accumalated fee
                fee_currency=data.feeCcy,  # accumalated fee currency
                cost=Decimal(data.avgPx) * Decimal(data.fillSz) * ct_val,
                cum_cost=Decimal(data.avgPx) * Decimal(data.accFillSz) * ct_val,
                reduce_only=data.reduceOnly,
                position_side=OkxEnumParser.parse_position_side(data.posSide),
            )
            return order
        except Exception as e:
            error_msg = f"{e.__class__.__name__}: {str(e)}"
            self._log.error(f"Error modifying order: {error_msg}")
            return None

    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
        try:
            res: OkxOrderResponse = await self._api_client.get_api_v5_trade_order(
                inst_id=market.id, cl_ord_id=oid
            )
            if not res.data:
                return None
            return self._order_data_to_order(symbol, res.data[0])
        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 []
        try:
            res: OkxOrderResponse = (
                await self._api_client.get_api_v5_trade_orders_pending(
                    inst_id=market.id
                )
            )
            orders: list[Order] = []
            for item in res.data:
                orders.append(self._order_data_to_order(symbol, item))
            return orders
        except Exception as e:
            self._log.error(f"fetch_open_orders failed for {symbol}: {e}")
            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"OKX 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"OKX reconnect resync failed: {e}")
            return {
                "success": False,
                "error": str(e),
                "positions_opened": [],
                "positions_closed": [],
                "open_orders_added": [],
                "open_orders_removed": [],
            }

    async def cancel_all_orders(self, symbol: str):
        """
        no cancel all orders in OKX
        """
        pass
