import hashlib
import hmac
import time
import urllib.parse
from copy import copy
from collections.abc import Callable
from time import sleep
from datetime import datetime, timedelta

from numpy import format_float_positional

from vnpy.event import EventEngine, Event
from vnpy.trader.event import EVENT_TIMER
from vnpy.trader.constant import (
    Direction,
    Exchange,
    Product,
    Status,
    OrderType,
    Interval
)
from vnpy.trader.gateway import BaseGateway
from vnpy.trader.object import (
    TickData,
    OrderData,
    TradeData,
    AccountData,
    ContractData,
    BarData,
    OrderRequest,
    CancelRequest,
    SubscribeRequest,
    HistoryRequest
)
from vnpy.trader.utility import ZoneInfo
from vnpy_rest import Request, RestClient, Response
from vnpy_websocket import WebsocketClient


# Timezone constant
UTC_TZ = ZoneInfo("UTC")

# Real server hosts
REAL_REST_HOST: str = "https://api.binance.com"
REAL_TRADE_HOST: str = "wss://ws-api.binance.com/ws-api/v3"
REAL_DATA_HOST: str = "wss://stream.binance.com:443"

# Testnet server hosts
TESTNET_REST_HOST: str = "https://testnet.binance.vision"
TESTNET_TRADE_HOST: str = "wss://ws-api.testnet.binance.vision/ws-api/v3"
TESTNET_DATA_HOST: str = "wss://stream.testnet.binance.vision/ws"

# Order status map
STATUS_BINANCE2VT: dict[str, Status] = {
    "NEW": Status.NOTTRADED,
    "PARTIALLY_FILLED": Status.PARTTRADED,
    "FILLED": Status.ALLTRADED,
    "CANCELED": Status.CANCELLED,
    "REJECTED": Status.REJECTED,
    "EXPIRED": Status.CANCELLED
}

# Order type map
ORDERTYPE_VT2BINANCE: dict[OrderType, tuple[str, str]] = {
    OrderType.LIMIT: ("LIMIT", "GTC"),
    OrderType.MARKET: ("MARKET", "GTC"),
    OrderType.FAK: ("LIMIT", "IOC"),
    OrderType.FOK: ("LIMIT", "FOK"),
}
ORDERTYPE_BINANCE2VT: dict[tuple[str, str], OrderType] = {v: k for k, v in ORDERTYPE_VT2BINANCE.items()}

# Direction map
DIRECTION_VT2BINANCE: dict[Direction, str] = {
    Direction.LONG: "BUY",
    Direction.SHORT: "SELL"
}
DIRECTION_BINANCE2VT: dict[str, Direction] = {v: k for k, v in DIRECTION_VT2BINANCE.items()}

# Kline interval map
INTERVAL_VT2BINANCE: dict[Interval, str] = {
    Interval.MINUTE: "1m",
    Interval.HOUR: "1h",
    Interval.DAILY: "1d",
}

# Timedelta map
TIMEDELTA_MAP: dict[Interval, timedelta] = {
    Interval.MINUTE: timedelta(minutes=1),
    Interval.HOUR: timedelta(hours=1),
    Interval.DAILY: timedelta(days=1),
}

# Set weboscket timeout to 24 hour
WEBSOCKET_TIMEOUT = 24 * 60 * 60


class BinanceSpotGateway(BaseGateway):
    """
    The Binance spot trading gateway for VeighNa.

    This gateway provides trading functionality for Binance spot markets
    through their API.

    Features:
    1. Provides market data, trading, and account management capabilities
    2. Supports limit, market, FOK, and FAK order types
    3. Handles real-time account balance updates
    """

    default_name: str = "BINANCE_SPOT"

    default_setting: dict = {
        "API Key": "",
        "API Secret": "",
        "Server": ["REAL", "TESTNET"],
        "Kline Stream": ["False", "True"],
        "Proxy Host": "",
        "Proxy Port": 0
    }

    exchanges: list[Exchange] = [Exchange.GLOBAL]

    def __init__(self, event_engine: EventEngine, gateway_name: str) -> None:
        """
        The init method of the gateway.

        This method initializes the gateway components including REST API,
        trading API, user data API, and market data API. It also sets up
        the data structures for order and contract storage.

        Parameters:
            event_engine: the global event engine object of VeighNa
            gateway_name: the unique name for identifying the gateway
        """
        super().__init__(event_engine, gateway_name)

        self.trade_api: TradeApi = TradeApi(self)
        self.md_api: MdApi = MdApi(self)
        self.rest_api: RestApi = RestApi(self)

        self.orders: dict[str, OrderData] = {}
        self.symbol_contract_map: dict[str, ContractData] = {}
        self.name_contract_map: dict[str, ContractData] = {}

    def connect(self, setting: dict) -> None:
        """
        Start server connections.

        This method establishes connections to Binance servers
        using the provided settings.

        Parameters:
            setting: A dictionary containing connection parameters including
                    API credentials, server selection, and proxy configuration
        """
        key: str = setting["API Key"]
        secret: str = setting["API Secret"]
        server: str = setting["Server"]
        kline_stream: bool = setting["Kline Stream"] == "True"
        proxy_host: str = setting["Proxy Host"]
        proxy_port: int = setting["Proxy Port"]

        self.rest_api.connect(key, secret, server, proxy_host, proxy_port)
        self.trade_api.connect(key, secret, server, proxy_host, proxy_port)
        self.md_api.connect(server, kline_stream, proxy_host, proxy_port)

        self.event_engine.register(EVENT_TIMER, self.process_timer_event)

    def subscribe(self, req: SubscribeRequest) -> None:
        """
        Subscribe to market data.

        This method forwards the subscription request to the market data API.

        Parameters:
            req: Subscription request object containing the symbol to subscribe
        """
        self.md_api.subscribe(req)

    def send_order(self, req: OrderRequest) -> str:
        """
        Send new order.

        This method forwards the order request to the trading API.

        Parameters:
            req: Order request object containing order details

        Returns:
            str: The VeighNa order ID if successful, empty string if failed
        """
        return self.trade_api.send_order(req)

    def cancel_order(self, req: CancelRequest) -> None:
        """
        Cancel existing order.

        This method forwards the cancellation request to the trading API.

        Parameters:
            req: Cancel request object containing order details
        """
        self.trade_api.cancel_order(req)

    def query_account(self) -> None:
        """
        Query account balance.

        Not required since Binance provides websocket updates for account balances.
        """
        pass

    def query_position(self) -> None:
        """
        Query current positions.

        Not implemented for spot trading as there is no position concept.
        """
        pass

    def query_history(self, req: HistoryRequest) -> list[BarData]:
        """
        Query historical kline data.

        This method forwards the history request to the REST API.

        Parameters:
            req: History request object containing query parameters

        Returns:
            list[BarData]: List of historical kline data bars
        """
        return self.rest_api.query_history(req)

    def close(self) -> None:
        """
        Close server connections.

        This method stops all API connections and releases resources.
        """
        self.rest_api.stop()
        self.md_api.stop()
        self.trade_api.stop()

    def on_order(self, order: OrderData) -> None:
        """
        Save a copy of order and then push to event engine.

        Parameters:
            order: Order data object
        """
        self.orders[order.orderid] = copy(order)
        super().on_order(order)

    def get_order(self, orderid: str) -> OrderData | None:
        """
        Get previously saved order by order id.

        Parameters:
            orderid: The ID of the order to retrieve

        Returns:
            Order data object if found, None otherwise
        """
        return self.orders.get(orderid, None)

    def on_contract(self, contract: ContractData) -> None:
        """
        Save contract data in mappings and push to event engine.

        Parameters:
            contract: Contract data object
        """
        self.symbol_contract_map[contract.symbol] = contract
        self.name_contract_map[contract.name] = contract
        super().on_contract(contract)

    def get_contract_by_symbol(self, symbol: str) -> ContractData | None:
        """
        Get contract data by VeighNa symbol.

        Parameters:
            symbol: VeighNa symbol (e.g. "BTC_USDT_BINANCE")

        Returns:
            Contract data object if found, None otherwise
        """
        return self.symbol_contract_map.get(symbol, None)

    def get_contract_by_name(self, name: str) -> ContractData | None:
        """
        Get contract data by exchange symbol name.

        Parameters:
            name: Exchange symbol name (e.g. "BTCUSDT")

        Returns:
            Contract data object if found, None otherwise
        """
        return self.name_contract_map.get(name, None)

    def process_timer_event(self, event: Event) -> None:
        """
        Process timer task.

        This function is called regularly by the event engine to perform scheduled tasks,
        such as keeping the user stream alive.

        Parameters:
            event: Timer event object
        """
        self.md_api.subscribe_new_channels()


class RestApi(RestClient):
    """
    The REST API of BinanceSpotGateway.

    This class handles HTTP requests to Binance API endpoints, including:
    - Authentication and signature generation
    - Contract information queries
    - Account balance queries
    - Order management
    - Historical data queries
    - User data stream management
    """

    def __init__(self, gateway: BinanceSpotGateway) -> None:
        """
        The init method of the API.

        This method initializes the REST API with a reference to the parent gateway.

        Parameters:
            gateway: the parent gateway object for pushing callback data
        """
        super().__init__()

        self.gateway: BinanceSpotGateway = gateway
        self.gateway_name: str = gateway.gateway_name

        self.key: str = ""
        self.secret: bytes = b""

        self.time_offset: int = 0

        self.order_count: int = 1_000_000
        self.order_prefix: str = ""

    def sign(self, request: Request) -> Request:
        """
        Standard callback for signing a request.

        This method adds the necessary authentication parameters and signature
        to requests that require API key authentication.

        It handles:
        1. Path construction with query parameters
        2. Timestamp generation with server time offset adjustment
        3. HMAC-SHA256 signature generation
        4. Required authentication headers

        Parameters:
            request: Request object to be signed

        Returns:
            Request: Modified request with authentication parameters
        """
        if request.data and request.data.get("signed", False):
            # Construct path with query parameters if they exist
            if request.params:
                path: str = request.path + "?" + urllib.parse.urlencode(request.params)
            else:
                request.params = {}
                path = request.path

            # Get current timestamp in milliseconds
            timestamp: int = int(time.time() * 1000)

            # Adjust timestamp based on time offset with server
            if self.time_offset > 0:
                timestamp -= abs(self.time_offset)
            elif self.time_offset < 0:
                timestamp += abs(self.time_offset)

            # Add timestamp to request parameters
            request.params["timestamp"] = timestamp

            # Generate signature using HMAC SHA256
            query: str = urllib.parse.urlencode(sorted(request.params.items()))
            signature: str = hmac.new(
                self.secret,
                query.encode("utf-8"),
                hashlib.sha256
            ).hexdigest()

            # Append signature to query string
            query += f"&signature={signature}"
            path = request.path + "?" + query

            # Update request with signed path and clear params/data
            request.path = path
            request.params = {}
            request.data = {}

        # Add required headers for API authentication
        request.headers = {
            "Content-Type": "application/x-www-form-urlencoded",
            "Accept": "application/json",
            "X-MBX-APIKEY": self.key,
            "Connection": "close"
        }

        return request

    def connect(
        self,
        key: str,
        secret: str,
        server: str,
        proxy_host: str,
        proxy_port: int
    ) -> None:
        """Start server connection"""
        self.key = key
        self.secret = secret.encode()
        self.proxy_port = proxy_port
        self.proxy_host = proxy_host
        self.server = server

        self.order_prefix = datetime.now().strftime("%y%m%d%H%M%S")

        if self.server == "REAL":
            self.init(REAL_REST_HOST, proxy_host, proxy_port)
        else:
            self.init(TESTNET_REST_HOST, proxy_host, proxy_port)

        self.start()

        self.gateway.write_log("REST API started")

        self.query_time()

    def query_time(self) -> None:
        """
        Query server time to calculate local time offset.

        This function sends a request to get the exchange server time,
        which is used to calculate the local time offset for timestamp synchronization.
        """
        path: str = "/api/v3/time"

        self.add_request(
            "GET",
            path,
            callback=self.on_query_time
        )

    def query_account(self) -> None:
        """
        Query account balance.

        This function sends a request to get the account balance information.
        """
        path: str = "/api/v3/account"

        self.add_request(
            method="GET",
            path=path,
            callback=self.on_query_account,
            data={"signed": True}
        )

    def query_order(self) -> None:
        """
        Query open orders.

        This function sends a request to get all active orders
        that have not been fully filled or cancelled.
        """
        path: str = "/api/v3/openOrders"

        self.add_request(
            method="GET",
            path=path,
            callback=self.on_query_order,
            data={"signed": True}
        )

    def query_contract(self) -> None:
        """
        Query available contracts.

        This function sends a request to get exchange information,
        including all available trading instruments, their precision,
        and trading rules.
        """
        path: str = "/api/v3/exchangeInfo"

        self.add_request(
            method="GET",
            path=path,
            callback=self.on_query_contract,
        )

    def on_query_time(self, data: dict, request: Request) -> None:
        """
        Callback of server time query.

        This function processes the server time response and calculates
        the time offset between local and server time, which is used for
        request timestamp synchronization.

        Parameters:
            data: Response data from the server
            request: Original request object
        """
        local_time: int = int(time.time() * 1000)
        server_time: int = int(data["serverTime"])
        self.time_offset = local_time - server_time

        self.gateway.write_log(f"Server time updated, local offset: {self.time_offset}ms")

        self.query_contract()

    def on_query_account(self, data: dict, request: Request) -> None:
        """
        Callback of account balance query.

        This function processes the account balance response and
        creates AccountData objects for each asset in the account.

        Parameters:
            data: Response data from the server
            request: Original request object
        """
        for balance in data["balances"]:
            asset = balance["asset"]
            free = float(balance["free"])
            locked = float(balance["locked"])

            if free or locked:
                account: AccountData = AccountData(
                    accountid=asset,
                    balance=free + locked,
                    frozen=locked,
                    gateway_name=self.gateway_name
                )
                self.gateway.on_account(account)

        self.gateway.write_log("Account data received")

    def on_query_order(self, data: list, request: Request) -> None:
        """
        Callback of open orders query.

        This function processes the open orders response and
        creates OrderData objects for each active order.

        Parameters:
            data: Response data from the server
            request: Original request object
        """
        for d in data:
            key: tuple[str, str] = (d["type"], d["timeInForce"])
            order_type: OrderType | None = ORDERTYPE_BINANCE2VT.get(key, None)
            if not order_type:
                continue

            contract: ContractData | None = self.gateway.get_contract_by_name(d["symbol"])
            if not contract:
                continue

            order: OrderData = OrderData(
                orderid=d["clientOrderId"],
                symbol=contract.symbol,
                exchange=Exchange.GLOBAL,
                price=float(d["price"]),
                volume=float(d["origQty"]),
                type=order_type,
                direction=DIRECTION_BINANCE2VT[d["side"]],
                traded=float(d["executedQty"]),
                status=STATUS_BINANCE2VT[d["status"]],
                datetime=generate_datetime(d["time"]),
                gateway_name=self.gateway_name,
            )
            self.gateway.on_order(order)

        self.gateway.write_log("Order data received")

    def on_query_contract(self, data: dict, request: Request) -> None:
        """
        Callback of available contracts query.

        This function processes the exchange info response and
        creates ContractData objects for each trading instrument.
        It extracts trading rules like price tick, minimum/maximum volumes from filters.

        Parameters:
            data: Response data from the server
            request: Original request object
        """
        for d in data["symbols"]:
            pricetick: float = 1
            min_volume: float = 1
            max_volume: float = 1

            for f in d["filters"]:
                if f["filterType"] == "PRICE_FILTER":
                    pricetick = float(f["tickSize"])
                elif f["filterType"] == "LOT_SIZE":
                    min_volume = float(f["minQty"])
                    max_volume = float(f["maxQty"])

            symbol: str = d["symbol"] + "_SPOT_BINANCE"

            contract: ContractData = ContractData(
                symbol=symbol,
                exchange=Exchange.GLOBAL,
                name=d["symbol"],
                pricetick=pricetick,
                size=1,
                min_volume=min_volume,
                max_volume=max_volume,
                product=Product.SPOT,
                net_position=True,
                history_data=True,
                gateway_name=self.gateway_name,
                stop_supported=False
            )
            self.gateway.on_contract(contract)

        self.gateway.write_log("Contract data received")

        # Query private data after time offset is calculated
        if self.key and self.secret:
            self.query_order()
            self.query_account()
            self.gateway.trade_api.subscribe_user_data_stream()

    def query_history(self, req: HistoryRequest) -> list[BarData]:
        """Query kline history data"""
        # Check if the contract and interval exist
        contract: ContractData | None = self.gateway.get_contract_by_symbol(req.symbol)
        if not contract:
            return []

        if not req.interval:
            return []

        # Prepare history list
        history: list[BarData] = []
        limit: int = 1000

        # Convert start time to milliseconds
        start_time: int = int(datetime.timestamp(req.start))

        while True:
            # Create query parameters
            params: dict = {
                "symbol": contract.name,
                "interval": INTERVAL_VT2BINANCE[req.interval],
                "limit": limit
            }

            params["startTime"] = start_time * 1000
            path: str = "/api/v3/klines"
            if req.end:
                end_time = int(datetime.timestamp(req.end))
                params["endTime"] = end_time * 1000     # Convert to milliseconds

            resp: Response = self.request(
                "GET",
                path=path,
                params=params
            )

            # Break the loop if request failed
            if resp.status_code // 100 != 2:
                msg: str = f"Query kline history failed, status code: {resp.status_code}, message: {resp.text}"
                self.gateway.write_log(msg)
                break
            else:
                data: dict = resp.json()
                if not data:
                    msg = f"No kline history data is received, start time: {start_time}"
                    self.gateway.write_log(msg)
                    break

                buf: list[BarData] = []

                for row in data:
                    bar: BarData = BarData(
                        symbol=req.symbol,
                        exchange=req.exchange,
                        datetime=generate_datetime(row[0]),
                        interval=req.interval,
                        volume=float(row[5]),
                        turnover=float(row[7]),
                        open_price=float(row[1]),
                        high_price=float(row[2]),
                        low_price=float(row[3]),
                        close_price=float(row[4]),
                        gateway_name=self.gateway_name
                    )
                    buf.append(bar)

                begin: datetime = buf[0].datetime
                end: datetime = buf[-1].datetime

                history.extend(buf)
                msg = f"Query kline history finished, {req.symbol} - {req.interval.value}, {begin} - {end}"
                self.gateway.write_log(msg)

                # Break the loop if the latest data received
                if (
                    len(data) < limit
                    or (req.end and end >= req.end)
                ):
                    break

                # Update query start time
                start_dt = bar.datetime + TIMEDELTA_MAP[req.interval]
                start_time = int(datetime.timestamp(start_dt))

            # Wait to meet request flow limit
            sleep(0.5)

        # Remove the unclosed kline
        if history:
            history.pop(-1)

        return history


class MdApi(WebsocketClient):
    """
    The market data websocket API of BinanceSpotGateway.

    This class handles market data from Binance through websocket connection.
    It processes real-time updates for:
    - Tickers (24hr statistics)
    - Order book depth (10 levels)
    - Klines (candlestick data) if enabled
    """

    def __init__(self, gateway: BinanceSpotGateway) -> None:
        """
        The init method of the API.

        This method initializes the websocket client with a reference to the parent gateway.

        Parameters:
            gateway: the parent gateway object for pushing callback data
        """
        super().__init__()

        self.gateway: BinanceSpotGateway = gateway
        self.gateway_name: str = gateway.gateway_name

        self.ticks: dict[str, TickData] = {}
        self.reqid: int = 0
        self.kline_stream: bool = False

        self.new_channels: list[str] = []

    def connect(
        self,
        server: str,
        kline_stream: bool,
        proxy_host: str,
        proxy_port: int,
    ) -> None:
        """
        Start server connection.

        This method establishes a websocket connection to Binance market data stream.

        Parameters:
            server: Server type ("REAL" or "TESTNET")
            kline_stream: Whether to include kline data stream
            proxy_host: Proxy server hostname or IP
            proxy_port: Proxy server port
        """
        self.kline_stream = kline_stream

        if server == "REAL":
            self.init(REAL_DATA_HOST, proxy_host, proxy_port, receive_timeout=WEBSOCKET_TIMEOUT)
        else:
            self.init(TESTNET_DATA_HOST, proxy_host, proxy_port, receive_timeout=WEBSOCKET_TIMEOUT)

        self.start()

    def on_connected(self) -> None:
        """
        Callback when server is connected.

        This function is called when the market data websocket connection
        is successfully established. It logs the connection status and
        resubscribes to previously subscribed market data channels.
        """
        self.gateway.write_log("MD API connected")

        # Resubscribe market data
        if self.ticks:
            channels = []
            for symbol in self.ticks.keys():
                channels.append(f"{symbol}@ticker")
                channels.append(f"{symbol}@bookTicker")

                if self.kline_stream:
                    channels.append(f"{symbol}@kline_1m")

            packet: dict = {
                "method": "SUBSCRIBE",
                "params": channels,
                "id": self.reqid
            }
            self.send_packet(packet)

    def subscribe(self, req: SubscribeRequest) -> None:
        """
        Subscribe to market data.

        This function sends subscription requests for ticker and depth data
        for the specified trading instrument. If kline_stream is enabled,
        it will also subscribe to 1-minute kline data.

        Parameters:
            req: Subscription request object containing symbol information
        """
        if req.symbol in self.ticks:
            return

        contract: ContractData | None = self.gateway.get_contract_by_symbol(req.symbol)
        if not contract:
            self.gateway.write_log(f"Failed to subscribe data, symbol not found: {req.symbol}")
            return

        self.reqid += 1

        # Initialize tick object
        tick: TickData = TickData(
            symbol=req.symbol,
            name=contract.name,
            exchange=Exchange.GLOBAL,
            datetime=datetime.now(UTC_TZ),
            gateway_name=self.gateway_name,
        )
        tick.extra = {}
        self.ticks[req.symbol] = tick

        channels: list[str] = [
            f"{contract.name.lower()}@ticker",
            f"{contract.name.lower()}@bookTicker"
        ]

        if self.kline_stream:
            channels.append(f"{contract.name.lower()}@kline_1m")

        self.new_channels.extend(channels)

    def subscribe_new_channels(self) -> None:
        """
        Update timer event.

        This function sends subscription requests for new channels
        to the market data websocket server.
        """
        if not self.new_channels:
            return

        packet: dict = {
            "method": "SUBSCRIBE",
            "params": self.new_channels,
            "id": self.reqid
        }
        self.send_packet(packet)

        self.new_channels = []

    def on_packet(self, packet: dict) -> None:
        """
        Callback of market data update.

        This function processes different types of market data updates,
        including ticker, depth, and kline data. It updates the corresponding
        TickData object and pushes updates to the gateway.

        Parameters:
            packet: JSON data received from websocket
        """
        name: str = packet.get("s", "")
        if not name:
            return

        contract: ContractData | None = self.gateway.get_contract_by_name(name.upper())
        if not contract:
            return
        tick: TickData = self.ticks[contract.symbol]

        channel: str = packet.get("e", "bookTicker")

        if channel == "24hrTicker":
            tick.volume = float(packet["v"])
            tick.turnover = float(packet["q"])
            tick.open_price = float(packet["o"])
            tick.high_price = float(packet["h"])
            tick.low_price = float(packet["l"])
            tick.last_price = float(packet["c"])
            tick.datetime = generate_datetime(float(packet["E"]))
        elif channel == "bookTicker":
            tick.bid_price_1 = float(packet["b"])
            tick.bid_volume_1 = float(packet["B"])
            tick.ask_price_1 = float(packet["a"])
            tick.ask_volume_1 = float(packet["A"])
        elif channel == "kline":  # Handle kline data
            kline_data: dict = packet["k"]

            # Check if bar is closed
            bar_ready: bool = kline_data.get("x", False)
            if not bar_ready:
                return

            if tick.extra is None:
                tick.extra = {}

            dt: datetime = generate_datetime(float(kline_data["t"]))

            tick.extra["bar"] = BarData(
                symbol=name.upper(),
                exchange=Exchange.GLOBAL,
                datetime=dt.replace(second=0, microsecond=0),
                interval=Interval.MINUTE,
                volume=float(kline_data["v"]),
                turnover=float(kline_data["q"]),
                open_price=float(kline_data["o"]),
                high_price=float(kline_data["h"]),
                low_price=float(kline_data["l"]),
                close_price=float(kline_data["c"]),
                gateway_name=self.gateway_name
            )

            # According to Binance API updates, /api/v3/myTrades now returns quoteQty
            if "Q" in kline_data:
                tick.extra["bar"].turnover = float(kline_data["Q"])

        if tick.last_price:
            tick.localtime = datetime.now()
            self.gateway.on_tick(copy(tick))

    def on_disconnected(self, status_code: int, msg: str) -> None:
        """
        Callback when server is disconnected.

        This function is called when the market data websocket connection
        is closed. It logs the disconnection details.

        Parameters:
            status_code: HTTP status code for the disconnection
            msg: Disconnection message
        """
        self.gateway.write_log(f"MD API disconnected, code: {status_code}, msg: {msg}")

    def on_error(self, e: Exception) -> None:
        """
        Callback when exception raised.

        This function is called when an exception occurs in the market data
        websocket connection. It logs the exception details for troubleshooting.

        Parameters:
            e: The exception that was raised
        """
        self.gateway.write_log(f"MD API exception: {e}")


class TradeApi(WebsocketClient):
    """
    The trading websocket API of BinanceSpotGateway.

    This class handles trading operations with Binance through websocket connection.
    It provides functionality for:
    - Order placement
    - Order cancellation
    - Request authentication and signature generation

    Note: According to Binance API updates from 2025-04-25, some request weights
    have been increased. For example, order amend operations now have a weight of 4
    instead of 1.
    """

    def __init__(self, gateway: BinanceSpotGateway) -> None:
        """
        The init method of the API.

        This method initializes the websocket client with a reference to the parent gateway.

        Parameters:
            gateway: the parent gateway object for pushing callback data
        """
        super().__init__()

        self.gateway: BinanceSpotGateway = gateway
        self.gateway_name: str = gateway.gateway_name

        self.key: str = ""
        self.secret: bytes = b""
        self.proxy_port: int = 0
        self.proxy_host: str = ""
        self.server: str = ""

        self.reqid: int = 0
        self.order_count: int = 0
        self.order_prefix: str = ""

        self.reqid_callback_map: dict[int, Callable] = {}
        self.reqid_order_map: dict[int, OrderData] = {}

        self.user_stream_subscribed: bool = False

    def connect(
        self,
        key: str,
        secret: str,
        server: str,
        proxy_host: str,
        proxy_port: int
    ) -> None:
        """
        Start server connection.

        This method initializes the API credentials and establishes
        a websocket connection to Binance trading API.

        Parameters:
            key: API Key for authentication
            secret: API Secret for request signing
            server: Server type ("REAL" or "TESTNET")
            proxy_host: Proxy server hostname or IP
            proxy_port: Proxy server port
        """
        self.key = key
        self.secret = secret.encode()
        self.proxy_port = proxy_port
        self.proxy_host = proxy_host
        self.server = server

        self.order_prefix = datetime.now().strftime("%y%m%d%H%M%S")

        if self.server == "REAL":
            self.init(REAL_TRADE_HOST, proxy_host, proxy_port)
        else:
            self.init(TESTNET_TRADE_HOST, proxy_host, proxy_port)

        self.start()

    def sign(self, params: dict) -> None:
        """
        Generate the signature for the request.

        This function creates an HMAC-SHA256 signature required for
        authenticated API requests to Binance.

        Parameters:
            params: Dictionary containing the parameters to be signed
        """
        timestamp: int = int(time.time() * 1000)
        params["timestamp"] = timestamp

        payload: str = "&".join([f"{k}={v}" for k, v in sorted(params.items())])
        signature: str = hmac.new(
            self.secret,
            payload.encode("utf-8"),
            hashlib.sha256
        ).hexdigest()
        params["signature"] = signature

    def send_order(self, req: OrderRequest) -> str:
        """
        Send new order to Binance.

        This function creates and sends a new order request to the exchange.
        It handles different order types including market and limit orders.

        Parameters:
            req: Order request object containing order details

        Returns:
            vt_orderid: The VeighNa order ID (gateway_name.orderid) if successful,
                       empty string otherwise
        """
        # Get contract
        contract: ContractData | None = self.gateway.get_contract_by_symbol(req.symbol)
        if not contract:
            self.gateway.write_log(f"Failed to send order, symbol not found: {req.symbol}")
            return ""

        # Generate new order id
        self.order_count += 1
        orderid: str = self.order_prefix + str(self.order_count)

        # Push a submitting order event
        order: OrderData = req.create_order_data(
            orderid,
            self.gateway_name
        )
        self.gateway.on_order(order)

        # Create order parameters
        params: dict = {
            "apiKey": self.key,
            "symbol": contract.name,
            "side": DIRECTION_VT2BINANCE[req.direction],
            "quantity": format_float(req.volume),
            "newClientOrderId": orderid,
        }

        if req.type == OrderType.MARKET:
            params["type"] = "MARKET"
        else:
            order_type, time_condition = ORDERTYPE_VT2BINANCE[req.type]
            params["type"] = order_type
            params["timeInForce"] = time_condition
            params["price"] = format_float(req.price)

        self.sign(params)

        self.reqid += 1
        self.reqid_callback_map[self.reqid] = self.on_send_order
        self.reqid_order_map[self.reqid] = order

        packet: dict = {
            "id": self.reqid,
            "method": "order.place",
            "params": params,
        }
        self.send_packet(packet)
        return order.vt_orderid

    def cancel_order(self, req: CancelRequest) -> None:
        """
        Cancel existing order on Binance.

        This function sends a request to cancel an existing order on the exchange.

        Parameters:
            req: Cancel request object containing order details
        """
        contract: ContractData | None = self.gateway.get_contract_by_symbol(req.symbol)
        if not contract:
            self.gateway.write_log(f"Failed to cancel order, symbol not found: {req.symbol}")
            return

        params: dict = {
            "apiKey": self.key,
            "symbol": contract.name,
            "origClientOrderId": req.orderid
        }
        self.sign(params)

        self.reqid += 1
        self.reqid_callback_map[self.reqid] = self.on_cancel_order

        packet: dict = {
            "id": self.reqid,
            "method": "order.cancel",
            "params": params,
        }
        self.send_packet(packet)

    def subscribe_user_data_stream(self) -> None:
        """
        Subscribe to user data stream.

        This function sends a subscription request to receive real-time
        account and order updates through the websocket API connection.
        """
        if not self.key or self.user_stream_subscribed:
            return

        self.user_stream_subscribed = True

        params: dict = {"apiKey": self.key}
        self.sign(params)

        self.reqid += 1
        self.reqid_callback_map[self.reqid] = self.on_subscribe_user_data_stream

        packet: dict = {
            "id": self.reqid,
            "method": "userDataStream.subscribe.signature",
            "params": params,
        }
        self.send_packet(packet)

    def on_subscribe_user_data_stream(self, packet: dict) -> None:
        """
        Callback of user data stream subscription.

        This function processes the response to the user data stream
        subscription request. It logs errors if the subscription fails.

        Parameters:
            packet: JSON data received from websocket
        """
        error: dict | None = packet.get("error", None)
        if error:
            self.user_stream_subscribed = False

            error_code: str = error["code"]
            error_msg: str = error["msg"]
            self.gateway.write_log(
                f"User data stream subscription failed, code: {error_code}, message: {error_msg}"
            )
            return

        self.gateway.write_log("User data stream subscribed")

    def on_connected(self) -> None:
        """
        Callback when server is connected.

        This function is called when the trading websocket connection
        is successfully established. It logs the connection status
        and subscribes to the user data stream.
        """
        self.gateway.write_log("Trade API connected")
        self.subscribe_user_data_stream()

    def on_disconnected(self, status_code: int, msg: str) -> None:
        """
        Callback when server is disconnected.

        This function is called when the trading websocket connection
        is closed. It logs the disconnection details.

        Parameters:
            status_code: HTTP status code for the disconnection
            msg: Disconnection message
        """
        self.gateway.write_log(f"Trade API disconnected, code: {status_code}, msg: {msg}")
        self.user_stream_subscribed = False

    def on_packet(self, packet: dict) -> None:
        """
        Callback of data update.

        This function processes responses from the trading websocket API
        and user data stream events. Trade responses are routed by request ID,
        while user data events are routed by event type.

        Parameters:
            packet: JSON data received from websocket
        """
        # User data stream event
        event: dict | None = packet.get("event", None)
        if event:
            self.on_user_data_event(event)
            return

        # Trade API response
        reqid: int = packet.get("id", 0)
        callback: Callable | None = self.reqid_callback_map.get(reqid, None)
        if callback:
            callback(packet)

    def on_user_data_event(self, event: dict) -> None:
        """
        Process user data stream event.

        This function routes user data stream events to the appropriate
        handler based on the event type.

        Parameters:
            event: Event payload from user data stream
        """
        match event["e"]:
            case "outboundAccountPosition":
                self.on_account(event)
            case "executionReport":
                self.on_order(event)

    def on_account(self, event: dict) -> None:
        """
        Callback of account balance update.

        This function processes the account update event from the user data stream,
        including balance changes.

        Parameters:
            event: Event payload from user data stream
        """
        for balance in event["B"]:
            asset = balance["a"]
            free = float(balance["f"])
            locked = float(balance["l"])

            if free or locked:
                account: AccountData = AccountData(
                    accountid=asset,
                    balance=free + locked,
                    frozen=locked,
                    gateway_name=self.gateway_name
                )
                self.gateway.on_account(account)

    def on_order(self, event: dict) -> None:
        """
        Callback of order and trade update.

        This function processes the order update event from the user data stream,
        including order status changes and trade executions.

        Parameters:
            event: Event payload from user data stream
        """
        symbol: str = event["s"]

        if not event["C"]:
            orderid: str = event["c"]
        else:
            orderid = event["C"]

        type_str: str = event["o"]
        time_in_force: str = event["f"]
        key: tuple[str, str] = (type_str, time_in_force)
        order_type: OrderType | None = ORDERTYPE_BINANCE2VT.get(key, None)
        if not order_type:
            return

        contract = self.gateway.get_contract_by_name(symbol)
        if not contract:
            return

        order = OrderData(
            symbol=contract.symbol,
            exchange=Exchange.GLOBAL,
            orderid=orderid,
            type=order_type,
            direction=DIRECTION_BINANCE2VT[event["S"]],
            price=float(event["p"]),
            volume=float(event["q"]),
            traded=float(event["z"]),
            status=STATUS_BINANCE2VT[event["X"]],
            datetime=generate_datetime(event["E"]),
            gateway_name=self.gateway_name,
        )
        self.gateway.on_order(order)

        if float(event["l"]) <= 0:
            return

        trade_volume = float(event["l"])
        trade = TradeData(
            symbol=contract.symbol,
            exchange=Exchange.GLOBAL,
            orderid=orderid,
            tradeid=event["t"],
            direction=DIRECTION_BINANCE2VT[event["S"]],
            price=float(event["L"]),
            volume=trade_volume,
            datetime=generate_datetime(event["T"]),
            gateway_name=self.gateway_name,
        )
        self.gateway.on_trade(trade)

    def on_send_order(self, packet: dict) -> None:
        """
        Callback of send order.

        This function processes the response to an order placement request.
        It handles errors by logging the details and updating the order status.

        Parameters:
            packet: JSON data received from websocket
        """
        error: dict | None = packet.get("error", None)
        if not error:
            return

        error_code: str = error["code"]
        error_msg: str = error["msg"]

        msg: str = f"Order rejected, code: {error_code}, message: {error_msg}"
        self.gateway.write_log(msg)

        reqid: int = packet.get("id", 0)
        order: OrderData | None = self.reqid_order_map.get(reqid, None)
        if order:
            order.status = Status.REJECTED
            self.gateway.on_order(order)

    def on_cancel_order(self, packet: dict) -> None:
        """
        Callback of cancel order.

        This function processes the response to an order cancellation request.
        It handles errors by logging the details.

        Parameters:
            packet: JSON data received from websocket
        """
        error: dict | None = packet.get("error", None)
        if not error:
            return

        error_code: str = error["code"]
        error_msg: str = error["msg"]

        msg: str = f"Cancel rejected, code: {error_code}, message: {error_msg}"
        self.gateway.write_log(msg)

    def on_error(self, e: Exception) -> None:
        """
        Callback when exception raised.

        This function is called when an exception occurs in the trading
        websocket connection. It logs the exception details for troubleshooting.

        Parameters:
            e: The exception that was raised
        """
        self.gateway.write_log(f"Trade API exception: {e}")


def generate_datetime(timestamp: float) -> datetime:
    """
    Generate datetime object from Binance timestamp.

    This function converts a Binance millisecond timestamp to a datetime object
    with UTC timezone.

    Parameters:
        timestamp: Binance timestamp in milliseconds

    Returns:
        Datetime object with UTC timezone
    """
    dt: datetime = datetime.fromtimestamp(timestamp / 1000, tz=UTC_TZ)
    return dt


def format_float(f: float) -> str:
    """
    Convert float number to string with correct precision.

    This function formats floating point numbers to avoid precision errors
    when sending requests to Binance.

    Parameters:
        f: The floating point number to format

    Returns:
        Formatted string representation of the number

    Note:
        Fixes potential error -1111: Parameter "quantity" has too much precision
    """
    return format_float_positional(f, trim="-")
