# piker: trading gear for hackers
# Copyright (C)
#  Guillermo Rodriguez (aka ze jefe)
#  Tyler Goodlet
#  (in stewardship for pikers)

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

"""
Binance clients for http and ws APIs.

"""
from __future__ import annotations
from collections import ChainMap
from contextlib import (
    asynccontextmanager as acm,
    AsyncExitStack,
)
from datetime import datetime
from pprint import pformat
from typing import (
    Any,
    Callable,
    Type,
)
import hmac
import hashlib
from pathlib import Path

import trio
from pendulum import (
    now,
)
import httpx
import numpy as np

from piker import config
from piker.clearing._messages import (
    Order,
)
from piker.accounting import (
    Asset,
    digits_to_dec,
    MktPair,
)
from piker.types import Struct
from piker.data import (
    def_iohlcv_fields,
    match_from_pairs,
)
from piker.brokers import (
    resproc,
    SymbolNotFound,
    get_logger,
)
from .venues import (
    PAIRTYPES,
    Pair,
    MarketType,
    _spot_url,
    _futes_url,
    _testnet_futes_url,
    _testnet_spot_url,
)

log = get_logger('piker.brokers.binance')


def get_config() -> dict[str, Any]:
    conf: dict
    path: Path
    conf, path = config.load(
        conf_name='brokers',
        touch_if_dne=True,
    )
    section: dict = conf.get('binance')
    if not section:
        log.warning(
            f'No config section found for binance in {path}'
        )
        return {}

    return section


log = get_logger(__name__)

# Broker specific ohlc schema (rest)
# XXX TODO? some additional fields are defined in the docs:
# https://binance-docs.github.io/apidocs/spot/en/#kline-candlestick-data

# _ohlc_dtype = [
    # ('close_time', int),
    # ('quote_vol', float),
    # ('num_trades', int),
    # ('buy_base_vol', float),
    # ('buy_quote_vol', float),
    # ('ignore', float),
# ]


class OHLC(Struct):
    '''
    Description of the flattened OHLC quote format.

    For schema details see:
    https://binance-docs.github.io/apidocs/spot/en/#kline-candlestick-streams

    '''
    time: int  # epoch in ms

    open: float
    high: float
    low: float
    close: float
    volume: float

    close_time: int

    quote_vol: float
    num_trades: int
    buy_base_vol: float
    buy_quote_vol: float
    ignore: int


# convert datetime obj timestamp to unixtime in milliseconds
def binance_timestamp(
    when: datetime
) -> int:
    return int((when.timestamp() * 1000) + (when.microsecond / 1000))


class Client:
    '''
    Async ReST API client using `trio` + `httpx` B)

    Supports all of the spot, margin and futures endpoints depending
    on method.

    '''
    def __init__(
        self,

        venue_sessions: dict[
            str,  # venue key
            tuple[httpx.AsyncClient, str]  # session, eps path
        ],
        conf: dict[str, Any],
        # TODO: change this to `Client.[mkt_]venue: MarketType`?
        mkt_mode: MarketType = 'spot',

    ) -> None:
        self.conf = conf

        # build out pair info tables for each market type
        # and wrap in a chain-map view for search / query.
        self._spot_pairs: dict[str, Pair] = {}  # spot info table
        self._ufutes_pairs: dict[str, Pair] = {}  # usd-futures table
        self._venue2pairs: dict[str, dict] = {
            'spot': self._spot_pairs,
            'usdtm_futes': self._ufutes_pairs,
        }

        self._venue2assets: dict[
            str,
            dict[str, dict] | None,
        ] = {
            # NOTE: only the spot table contains a dict[str, Asset]
            # since others (like futes, opts) can just do lookups
            # from a list of names to the spot equivalent.
            'spot': {},
            'usdtm_futes': {},
            # 'coinm_futes': {},
        }

        # NOTE: only stick in the spot table for now until exchange info
        # is loaded, since at that point we'll suffix all the futes
        # market symbols for use by search. See `.exch_info()`.
        self._pairs: ChainMap[str, Pair] = ChainMap()

        # global client "venue selection" mode.
        # set this when you want to switch venues and not have to
        # specify the venue for the next request.
        self.mkt_mode: MarketType = mkt_mode

        # per-mkt-venue API client table
        self.venue_sesh = venue_sessions

        # lookup for going from `.mkt_mode: str` to the config
        # subsection `key: str`
        self.venue2configkey: dict[str, str] = {
            'spot': 'spot',
            'margin': 'spot',
            'usdtm_futes': 'futes',
            # 'coinm_futes': 'futes',
        }
        self.confkey2venuekeys: dict[str, list[str]] = {
            'spot': ['spot'], # 'margin'],
            'futes': ['usdtm_futes'],
        }

    def _mk_sig(
        self,
        data: dict,
        venue: str,

    ) -> str:

        # look up subconfig (spot or futes) section using 
        # venue specific key lookup to figure out which mkt
        # we need a key for.
        section_name: str = self.venue2configkey[venue]
        subconf: dict | None = self.conf.get(section_name)
        if subconf is None:
            raise config.ConfigurationError(
                f'binance configuration is missing a `{section_name}` section '
                'to define the creds for auth-ed endpoints!?'
            )

        # XXX: Info on security and authentification
        # https://binance-docs.github.io/apidocs/#endpoint-security-type
        if not (api_secret := subconf.get('api_secret')):
            raise config.NoSignature(
                "Can't generate a signature without setting up credentials"
            )

        query_str: str = '&'.join([
            f'{key}={value}'
            for key, value in data.items()
        ])

        msg_auth = hmac.new(
            api_secret.encode('utf-8'),
            query_str.encode('utf-8'),
            hashlib.sha256
        )
        return msg_auth.hexdigest()

    # TODO: factor all these _api methods into a single impl
    # which looks up the parent path for eps depending on a
    # mkt_mode: MarketType input!
    async def _api(
        self,
        endpoint: str,  # ReST endpoint key
        params: dict,

        method: str = 'get',
        venue: str|None = None,  # if None use `.mkt_mode` state
        signed: bool = False,
        allow_testnet: bool = False,

    ) -> dict[str, Any]:
        '''
        Make a ReST API request via
        - a /api/v3/ SPOT, or
        - /fapi/v3/ USD-M FUTURES, or
        - /api/v3/ SPOT/MARGIN

        account/market endpoint request depending on either passed in
        `venue: str` or the current setting `.mkt_mode: str` setting,
        default `'spot'`.


        Docs per venue API:

        SPOT: market-data and spot-account-trade eps use this
        ----  endpoing parent path:
        - https://binance-docs.github.io/apidocs/spot/en/#market-data-endpoints
        - https://binance-docs.github.io/apidocs/spot/en/#spot-account-trade

        MARGIN: and advancecd spot account eps:
        ------
        - https://binance-docs.github.io/apidocs/spot/en/#margin-account-trade
        - https://binance-docs.github.io/apidocs/spot/en/#listen-key-spot
        - https://binance-docs.github.io/apidocs/spot/en/#spot-algo-endpoints

        USD-M FUTES:
        -----------
        - https://binance-docs.github.io/apidocs/futures/en/#market-data-endpoints

        '''
        venue_key: str = venue or self.mkt_mode

        if signed:
            params['signature'] = self._mk_sig(
                params,
                venue=venue_key,
            )

        # Check if we're configured to route order requests to the
        # venue equivalent's testnet.
        use_testnet: bool = False
        section_name: str = self.venue2configkey[venue_key]
        if subconf := self.conf.get(section_name):
            use_testnet = (
                subconf.get('use_testnet', False)
                and allow_testnet
            )

        if (
            use_testnet
            and method not in {
                'klines',
                'exchangeInfo',
            }
        ):
            # NOTE: only use testnet if user set brokers.toml config
            # var to true **and** it's not one of the market data
            # endpoints since we basically never want to display the
            # test net feeds, we only are using it for testing order
            # ctl machinery B)
            venue_key += '_testnet'

        client: httpx.AsyncClient
        path: str
        client, path = self.venue_sesh[venue_key]
        meth: Callable = getattr(client, method)
        resp = await meth(
            url=path + endpoint,
            params=params,
            timeout=float('inf'),
        )
        return resproc(resp, log)

    async def _cache_pairs(
        self,
        venue: str,

    ) -> None:
        # lookup internal mkt-specific pair table to update
        pair_table: dict[str, Pair] = self._venue2pairs[venue]

        # make API request(s)
        resp = await self._api(
            'exchangeInfo',
            params={},  # NOTE: retrieve all symbols by default
            # XXX: MUST explicitly pass the routing venue since we
            # don't know the routing mode but want to cache market
            # infos across all venues
            venue=venue,
            allow_testnet=False,  # XXX: never use testnet for symbol lookups
        )

        mkt_pairs = resp['symbols']
        if not mkt_pairs:
            raise SymbolNotFound(f'No market pairs found!?:\n{resp}')

        pairs_view_subtable: dict[str, Pair] = {}

        for item in mkt_pairs:
            filters_ls: list = item.pop('filters', False)
            if filters_ls:
                filters = {}
                for entry in filters_ls:
                    ftype = entry['filterType']
                    filters[ftype] = entry

                item['filters'] = filters

            pair_type: Type = PAIRTYPES[venue]
            try:
                pair: Pair = pair_type(**item)
            except Exception as e:
                e.add_note(
                    f'\n'
                    f'New or removed field we need to codify!\n'
                    f'pair-type: {pair_type!r}\n'
                    f'\n'
                    f"Don't panic, prolly stupid binance changed their symbology schema again..\n"
                    f'Check out their API docs here:\n'
                    f'\n'
                    f'https://binance-docs.github.io/apidocs/spot/en/#exchange-information\n'
                )
                raise
            pair_table[pair.symbol.upper()] = pair

            # update an additional top-level-cross-venue-table
            # `._pairs: ChainMap` for search B0
            pairs_view_subtable[pair.bs_fqme] = pair

            # XXX WOW: TURNS OUT THIS ISN'T TRUE !?
            # > (populate `Asset` table for spot mkts only since it
            # > should be a superset of any other venues such as
            # > futes or margin)
            if venue == 'spot':
                dst_sectype: str = 'crypto_currency'

            elif venue in {'usdtm_futes'}:
                dst_sectype: str = 'future'
                if pair.contractType == 'PERPETUAL':
                    dst_sectype: str = 'perpetual_future'

            spot_asset_table: dict[str, Asset] = self._venue2assets['spot']
            ven_asset_table: dict[str, Asset] = self._venue2assets[venue]

            if (
                (name := pair.quoteAsset) not in spot_asset_table
            ):
                spot_asset_table[pair.bs_src_asset] = Asset(
                    name=name,
                    atype='crypto_currency',
                    tx_tick=digits_to_dec(pair.quoteAssetPrecision),
                )

            if (
                (name := pair.baseAsset) not in ven_asset_table
            ):
                if venue != 'spot':
                    assert dst_sectype != 'crypto_currency'

                ven_asset_table[pair.bs_dst_asset] = Asset(
                    name=name,
                    atype=dst_sectype,
                    tx_tick=digits_to_dec(pair.baseAssetPrecision),
                )

        # log.warning(
        #     f'Assets not YET found in spot set: `{pformat(dne)}`!?'
        # )
        # NOTE: make merged view of all market-type pairs but
        # use market specific `Pair.bs_fqme` for keys!
        # this allows searching for market pairs with different
        # suffixes easily, for ex. `BTCUSDT.USDTM.PERP` will show
        # up when a user uses the search endpoint with pattern
        # `btc` B)
        self._pairs.maps.append(pairs_view_subtable)

        if venue == 'spot':
            return

        # TODO: maybe use this assets response for non-spot venues?
        # -> issue is we do the exch_info queries conc, so we can't
        # guarantee order for inter-table lookups..
        # if venue ep delivers an explicit set of assets copy just
        # ensure they are also already listed in the spot equivs.
        # assets: list[dict] = resp.get('assets', ())
        # for entry in assets:
        #     name: str = entry['asset']
        #     spot_asset_table: dict[str, Asset] = self._venue2assets['spot']
        #     if name not in spot_asset_table:
        #         log.warning(
        #             f'COULDNT FIND ASSET {name}\n{entry}\n'
        #             f'ADDING AS FUTES ONLY!?'
        #         )
        #     asset_table: dict[str, Asset] = self._venue2assets[venue]
        #     asset_table[name] = spot_asset_table.get(name)

    async def exch_info(
        self,
        sym: str | None = None,

        venue: MarketType | None = None,
        expiry: str | None = None,

    ) -> dict[str, Pair] | Pair:
        '''
        Fresh exchange-pairs info query for symbol ``sym: str``.

        Depending on `mkt_type` different api eps are used:
        - spot:
          https://binance-docs.github.io/apidocs/spot/en/#exchange-information
        - usd futes:
          https://binance-docs.github.io/apidocs/futures/en/#check-server-time
        - coin futes:
          https://binance-docs.github.io/apidocs/delivery/en/#exchange-information

        '''
        pair_table: dict[str, Pair] = self._venue2pairs[
            venue
            or
            self.mkt_mode
        ]
        if (
            expiry
            and 'perp' not in expiry.lower()
        ):
            sym: str = f'{sym}_{expiry}'

        if (
            sym
            and (cached_pair := pair_table.get(sym))
        ):
            return cached_pair

        venues: list[str] = ['spot', 'usdtm_futes']
        if venue:
            venues: list[str] = [venue]

        # batch per-venue download of all exchange infos
        async with trio.open_nursery() as tn:
            for ven in venues:
                tn.start_soon(
                    self._cache_pairs,
                    ven,
                )

        if sym:
            return pair_table[sym]
        else:
            return self._pairs

    async def get_assets(
        self,
        venue: str | None = None,

    ) -> dict[str, Asset]:
        if (
            venue
            and venue != 'spot'
        ):
            venues = [venue]
        else:
            venues = ['usdtm_futes']

        ass_table: dict[str, Asset] = self._venue2assets['spot']

        # merge in futes contracts with a sectype suffix
        for venue in venues:
            ass_table |= self._venue2assets[venue]

        return ass_table


    async def get_mkt_pairs(self) -> dict[str, Pair]:
        '''
        Flatten the multi-venue (chain) map of market pairs
        to a fqme indexed table for data layer caching.

        '''
        flat: dict[str, Pair] = {}
        for venmap in self._pairs.maps:
            for bs_fqme, pair in venmap.items():
                flat[pair.bs_fqme] = pair

        return flat

    # TODO: unused except by `brokers.core.search_symbols()`?
    async def search_symbols(
        self,
        pattern: str,
        limit: int = None,

    ) -> dict[str, Any]:

        fq_pairs: dict[str, Pair] = await self.exch_info()

        # TODO: cache this list like we were in
        # `open_symbol_search()`?
        # keys: list[str] = list(fq_pairs)

        return match_from_pairs(
            pairs=fq_pairs,
            query=pattern.upper(),
            score_cutoff=50,
        )

    def pair2venuekey(
        self,
        pair: Pair,
    ) -> str:
        return {
            'USDTM': 'usdtm_futes',
            'SPOT': 'spot',
            # 'COINM': 'coin_futes',
            # ^-TODO-^ bc someone might want it..?
        }[pair.venue]

    async def bars(
        self,
        mkt: MktPair,

        start_dt: datetime | None = None,
        end_dt: datetime | None = None,

        as_np: bool = True,

    ) -> list[tuple] | np.ndarray:

        # NOTE: diff market-venues have diff datums limits:
        # - spot max is 1k
        #   https://binance-docs.github.io/apidocs/spot/en/#kline-candlestick-data
        # - usdm futes max is 1500
        #   https://binance-docs.github.io/apidocs/futures/en/#kline-candlestick-data
        limits: dict[str, int] = {
            'spot': 1000,
            'usdtm_futes': 1500,
        }
        limit = limits[self.mkt_mode]

        if end_dt is None:
            end_dt = now('UTC').add(minutes=1)

        if start_dt is None:
            start_dt = end_dt.start_of(
                'minute').subtract(minutes=limit)

        start_time = binance_timestamp(start_dt)
        end_time = binance_timestamp(end_dt)

        bs_pair: Pair = self._pairs[mkt.bs_fqme.upper()]

        # https://binance-docs.github.io/apidocs/spot/en/#kline-candlestick-data
        bars = await self._api(
            'klines',
            params={
                # NOTE: always query using their native symbology!
                'symbol': mkt.bs_mktid.upper(),
                'interval': '1m',
                'startTime': start_time,
                'endTime': end_time,
                'limit': limit
            },
            venue=self.pair2venuekey(bs_pair),
            allow_testnet=False,
        )
        new_bars: list[tuple] = []
        for i, bar_list in enumerate(bars):

            bar = OHLC(*bar_list)
            bar.typecast()

            row = []
            for j, (name, ftype) in enumerate(def_iohlcv_fields[1:]):

                # TODO: maybe we should go nanoseconds on all
                # history time stamps?
                if name == 'time':
                    # convert to epoch seconds: float
                    row.append(bar.time / 1000.0)

                else:
                    row.append(getattr(bar, name))

            new_bars.append((i,) + tuple(row))

        if not as_np:
            return bars

        return np.array(
            new_bars,
            dtype=def_iohlcv_fields,
        )

    # TODO: maybe drop? Do we need this if we can simply request it
    # over the user stream wss?
    # async def get_positions(
    #     self,
    #     symbol: str,
    #     recv_window: int = 60000

    # ) -> tuple:

    #     positions = {}
    #     volumes = {}

    #     params = dict([
    #         ('symbol', symbol),
    #         ('recvWindow', recv_window),
    #         ('timestamp', binance_timestamp(now()))
    #     ])
    #     resp = await self._api(
    #         'allOrders',
    #         params=params,
    #         signed=True
    #     )
    #     log.info(f'done. len {len(resp)}')

    #     return positions, volumes

    async def get_deposits(
        self,
        recv_window: int = 60000
    ) -> list:

        # TODO: can't we drop this since normal dicts are
        # ordered implicitly in mordern python?
        params = dict([
            ('recvWindow', recv_window),
            ('timestamp', binance_timestamp(now()))
        ])
        return await self._sapi(
            'capital/deposit/hisrec',
            params=params,
            signed=True,
        )

    async def get_withdrawls(
        self,
        recv_window: int = 60000

    ) -> list:

        params = dict([
            ('recvWindow', recv_window),
            ('timestamp', binance_timestamp(now()))
        ])
        return await self._sapi(
            'capital/withdraw/history',
            params=params,
            signed=True,
        )

    async def get_open_orders(
        self,
        symbol: str | None = None,

    ) -> list[Order]:
        '''
        Get all open orders for venue-account.

        WARNING: apparently not specifying the symbol is given
        a much heavier API "weight" meaning you shouldn't call it
        often to avoid getting throttled as per:

        'https://binance-docs.github.io/apidocs/futures/en/#current-all-open-orders-user_data


        '''
        params: dict[str, Any] = {
            'timestamp': binance_timestamp(now()),
        }
        if symbol is not None:
            params['symbol'] = symbol

        resp = await self._api(
            'openOrders',
            params=params,
            signed=True,
            method='get',
            allow_testnet=True,
        )
        # figure out which venue (in FQME terms) we're using
        # since that normally maps 1-to-1 with the account (right?)
        venue: str = self.mkt_mode.rstrip('_futes')

        orders: list[Order] = []
        for entry in resp:
            oid: str = entry['clientOrderId']
            symbol: str = entry['symbol']

            # build out a fqme-styled key that should map to a pair
            # entry in `._pairs` cross-venue table.
            bs_mktid, _, expiry = entry['symbol'].partition('_')
            bs_mktid += f'.{venue.upper()}'

            if expiry:
                bs_mktid += f'.{expiry}'
            else:
                bs_mktid += '.PERP'

            # should never key error if we've got it right B)
            pair: Pair = self._pairs[bs_mktid]

            orders.append(
                Order(
                    oid=oid,
                    symbol=pair.bs_fqme.lower(),

                    action=entry['side'].lower(),
                    price=float(entry['price']),
                    size=float(entry['origQty']),

                    exec_mode='live',
                    account=f'binance.{venue}',
                )
            )
        return orders

    async def submit_limit(
        self,
        symbol: str,
        side: str,  # sell / buy
        quantity: float,
        price: float,

        oid: int | None = None,
        tif: str = 'GTC',
        recv_window: int = 60000,

        # iceberg_quantity: float | None = None,
        resp_type: str = 'ACK',

        # TODO: this is probably useful for doing stops, maybe we
        # can set it only on dark-stops?
        # close_all: bool = False,

        modify: bool = False,

    ) -> str:
        '''
        Submit or modify a live limit order to ze binance.

        For modify see:
        - spot: 
        - futes https://binance-docs.github.io/apidocs/futures/en/#modify-order-trade

        '''
        # lookup the binance-native symbol from search table
        bs_mktid: str = self._pairs[symbol.upper()].symbol
        params: dict = dict([
            ('symbol', bs_mktid),
            ('side', side.upper()),
            ('type', 'LIMIT'),
            ('timeInForce', tif),
            ('quantity', quantity),
            ('price', price),
            ('recvWindow', recv_window),
            ('newOrderRespType', resp_type),
            ('timestamp', binance_timestamp(now()))

            # ('closeAll', close_all),
        ])

        method: str = 'post'

        # NOTE: modifies only require diff key for user oid:
        # https://binance-docs.github.io/apidocs/futures/en/#modify-order-trade
        if modify:
            assert oid
            params['origClientOrderId'] = oid
            method: str = 'put'

        elif oid:
            params['newClientOrderId'] = oid

        log.info(
            'Submitting ReST order request:\n'
            f'{pformat(params)}'
        )
        resp = await self._api(
            'order',
            params=params,
            signed=True,
            method=method,
            venue=self.mkt_mode,
            allow_testnet=True,
        )

        # ensure our id is tracked by them
        if (
            oid
            and not modify
        ):
            assert oid == resp['clientOrderId']

        reqid: str = resp['orderId']
        return reqid

    async def submit_cancel(
        self,
        symbol: str,
        oid: str,

        recv_window: int = 60000

    ) -> None:
        bs_mktid: str = self._pairs[symbol.upper()].symbol
        params = dict([
            ('symbol', bs_mktid),
            # ('orderId', oid),
            ('origClientOrderId', oid),
            ('recvWindow', recv_window),
            ('timestamp', binance_timestamp(now()))
        ])

        log.cancel(
            'Submitting ReST order cancel: {oid}\n'
            f'{pformat(params)}'
        )
        await self._api(
            'order',
            params=params,
            signed=True,
            method='delete',
            allow_testnet=True,
        )

    async def get_listen_key(self) -> str:

        resp = await self._api(
            # 'userDataStream',  # spot
            'listenKey',
            params={},
            method='post',
            signed=True,
            allow_testnet=True,
        )
        return resp['listenKey']

    async def keep_alive_key(self, listen_key: str) -> None:
        await self._api(
            # 'userDataStream',
            'listenKey',
            params={'listenKey': listen_key},
            method='put',
            allow_testnet=True,
        )

    async def close_listen_key(self, listen_key: str) -> None:
        await self._api(
            # 'userDataStream',
            'listenKey',
            params={'listenKey': listen_key},
            method='delete',
            allow_testnet=True,
        )

    @acm
    async def manage_listen_key(self):

        async def periodic_keep_alive(
            self,
            listen_key: str,
            timeout=60 * 29  # 29 minutes
        ):
            while True:
                await trio.sleep(timeout)
                await self.keep_alive_key(listen_key)

        key = await self.get_listen_key()

        async with trio.open_nursery() as n:
            n.start_soon(periodic_keep_alive, self, key)
            yield key
            n.cancel_scope.cancel()

        await self.close_listen_key(key)


_venue_urls: dict[str, str] = {
    'spot': (
        _spot_url,
        '/api/v3/',
    ),
    'spot_testnet': (
        _testnet_spot_url,
        '/fapi/v1/'
    ),
    # margin and extended spot endpoints session.
    # TODO: did this ever get implemented fully?
    # 'margin': (
    #     _spot_url,
    #     '/sapi/v1/'
    # ),

    'usdtm_futes': (
        _futes_url,
        '/fapi/v1/',
    ),

    'usdtm_futes_testnet': (
        _testnet_futes_url,
        '/fapi/v1/',
    ),

    # TODO: for anyone who actually needs it ;P
    # 'coin_futes': ()
}


def init_api_keys(
    client: Client,
    conf: dict[str, Any],
) -> None:
    '''
    Set up per-venue API keys each http client according to the user's
    `brokers.conf`.

    For ex, to use spot-testnet and live usdt futures APIs:

    ```toml
        [binance]
        # spot test net
        spot.use_testnet = true
        spot.api_key = '<spot_api_key_from_binance_account>'
        spot.api_secret = '<spot_api_key_password>'

        # futes live
        futes.use_testnet = false
        accounts.usdtm = 'futes'
        futes.api_key = '<futes_api_key_from_binance>'
        futes.api_secret = '<futes_api_key_password>''

        # if uncommented will use the built-in paper engine and not
        # connect to `binance` API servers for order ctl.
        # accounts.paper = 'paper'
    ```

    '''
    for key, subconf in conf.items():
        if api_key := subconf.get('api_key', ''):
            venue_keys: list[str] = client.confkey2venuekeys[key]

            venue_key: str
            client: httpx.AsyncClient
            for venue_key in venue_keys:
                client, _ = client.venue_sesh[venue_key]

                api_key_header: dict = {
                    # taken from official:
                    # https://github.com/binance/binance-futures-connector-python/blob/main/binance/api.py#L47
                    "Content-Type": "application/json;charset=utf-8",

                    # TODO: prolly should just always query and copy
                    # in the real latest ver?
                    "User-Agent": "binance-connector/6.1.6smbz6",
                    "X-MBX-APIKEY": api_key,
                }
                client.headers.update(api_key_header)

                # if `.use_tesnet = true` in the config then
                # also add headers for the testnet session which
                # will be used for all order control
                if subconf.get('use_testnet', False):
                    testnet_sesh, _ = client.venue_sesh[
                        venue_key + '_testnet'
                    ]
                    testnet_sesh.headers.update(api_key_header)


@acm
async def get_client(
    mkt_mode: MarketType = 'spot',
) -> Client:
    '''
    Construct an single `piker` client which composes multiple underlying venue
    specific API clients both for live and test networks.

    '''
    venue_sessions: dict[
        str,  # venue key
        tuple[httpx.AsyncClient, str]  # session, eps path
    ] = {}
    async with AsyncExitStack() as client_stack:
        for name, (base_url, path) in _venue_urls.items():
            api: httpx.AsyncClient = await client_stack.enter_async_context(
                httpx.AsyncClient(
                    base_url=base_url,
                    # headers={},

                    # TODO: is there a way to numerate this?
                    # https://www.python-httpx.org/advanced/clients/#why-use-a-client
                    # connections=4
                )
            )
            venue_sessions[name] = (
                api,
                path,
            )

        conf: dict[str, Any] = get_config()
        # for creating API keys see,
        # https://www.binance.com/en/support/faq/how-to-create-api-keys-on-binance-360002502072
        client = Client(
            venue_sessions=venue_sessions,
            conf=conf,
            mkt_mode=mkt_mode,
        )
        init_api_keys(
            client=client,
            conf=conf,
        )
        fq_pairs: dict[str, Pair] = await client.exch_info()
        assert fq_pairs
        log.info(
            f'Loaded multi-venue `Client` in mkt_mode={client.mkt_mode!r}\n\n'
            f'Symbology Summary:\n'
            f'------ - ------\n'
            f'spot: {len(client._spot_pairs)}\n'
            f'usdtm_futes: {len(client._ufutes_pairs)}\n'
            '------ - ------\n'
            f'total: {len(client._pairs)}\n'
        )
        yield client
