# -*- coding: utf-8 -*-

# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:
# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code

import ccxt.async_support
from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolBySide
from ccxt.base.types import Any, Int, OrderBook, Position, Strings, Trade
from ccxt.async_support.base.ws.client import Client
from typing import List
from ccxt.base.errors import ExchangeError
from ccxt.base.errors import AuthenticationError


class aftermath(ccxt.async_support.aftermath):

    def describe(self) -> Any:
        return self.deep_extend(super(aftermath, self).describe(), {
            'has': {
                'ws': True,
                'watchBalance': False,
                'watchMyTrades': False,
                'watchOHLCV': False,
                'watchOrderBook': True,
                'watchOrders': False,
                'watchTicker': False,
                'watchTickers': False,
                'watchBidsAsks': False,
                'watchTrades': True,
                'watchTradesForSymbols': False,
                'watchPositions': True,
            },
            'urls': {
                'api': {
                    'ws': {
                        'swap': 'wss://aftermath.finance/iperps-api/ccxt/stream',
                    },
                },
                'test': {
                    'ws': {
                        'swap': 'wss://testnet.aftermath.finance/iperps-api/ccxt/stream',
                    },
                },
            },
            'options': {
                'tradesLimit': 1000,
                'ordersLimit': 1000,
                'watchPositions': {
                    'fetchPositionsSnapshot': True,  # or False
                    'awaitPositionsSnapshot': True,  # whether to wait for the positions snapshot before providing updates
                },
            },
            'streaming': {
                'keepAlive': 59000,
            },
            'exceptions': {
            },
        })

    async def watch_public(self, suffix, messageHash, message):
        url = self.urls['api']['ws']['swap'] + '/' + suffix
        return await self.watch(url, messageHash, self.json(message), messageHash, message)

    async def watch_public_multiple(self, suffix, messageHashes, message):
        url = self.urls['api']['ws']['swap'] + '/' + suffix
        return await self.watch_multiple(url, messageHashes, self.json(message), messageHashes, message)

    async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
        """
        watches information on multiple trades made in a market

        https://testnet.aftermath.finance/docs/#/CCXT/service%3A%3Ahandlers%3A%3Accxt%3A%3Astream%3A%3Atrades

        :param str symbol: unified market symbol of the market trades were made in
        :param int [since]: the earliest time in ms to fetch trades for
        :param int [limit]: the maximum number of trade structures to retrieve
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict[]: a list of `trade structures <https://docs.ccxt.com/?id=trade-structure>`
        """
        await self.load_markets()
        market = self.market(symbol)
        symbol = market['symbol']
        topic = market['id'] + '@trade'
        request: dict = {
            'chId': market['id'],
        }
        message = self.extend(request, params)
        trades = await self.watch_public('trades', topic, message)
        if self.newUpdates:
            limit = trades.getLimit(market['symbol'], limit)
        return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True)

    def handle_trade(self, client: Client, message):
        #
        # {
        #     "amount": 0.1,
        #     "cost": 0.1,
        #     "datetime": "string",
        #     "fee": null,
        #     "id": "string",
        #     "order": "string",
        #     "price": 0.1,
        #     "side": null,
        #     "symbol": "string",
        #     "takerOrMaker": null,
        #     "timestamp": null,
        #     "type": "string"
        # }
        #
        trade = self.parse_trade(message)
        symbol = trade['symbol']
        market = self.market(symbol)
        if not (symbol in self.trades):
            limit = self.safe_integer(self.options, 'tradesLimit', 1000)
            stored = ArrayCache(limit)
            self.trades[symbol] = stored
        messageHash = market['id'] + '@trade'
        trades = self.trades[symbol]
        trades.append(trade)
        self.trades[symbol] = trades
        client.resolve(trades, messageHash)

    async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:
        """

        https://testnet.aftermath.finance/docs/#/CCXT/service%3A%3Ahandlers%3A%3Accxt%3A%3Astream%3A%3Aorderbook

        watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data
        :param str symbol: unified symbol of the market to fetch the order book for
        :param int [limit]: the maximum amount of order book entries to return.
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: A dictionary of `order book structures <https://docs.ccxt.com/?id=order-book-structure>` indexed by market symbols
        """
        await self.load_markets()
        market = self.market(symbol)
        symbol = market['symbol']
        topic = market['id'] + '@orderbook'
        request: dict = {
            'chId': market['id'],
        }
        message = self.extend(request, params)
        orderbook = await self.watch_public('orderbook', topic, message)
        return orderbook.limit()

    def handle_order_book(self, client: Client, message):
        #
        # {
        #     "asks": [
        #       []
        #     ],
        #     "bids": [
        #       []
        #     ],
        #     "datetime": "string",
        #     "nonce": 9007199254740991,
        #     "symbol": "string",
        #     "timestamp": 9007199254740991
        # }
        #
        symbol = self.safe_string(message, 'symbol')
        market = self.market(symbol)
        topic = market['id'] + '@orderbook'
        if not (symbol in self.orderbooks):
            defaultLimit = self.safe_integer(self.options, 'watchOrderBookLimit', 1000)
            subscription = client.subscriptions[topic]
            limit = self.safe_integer(subscription, 'limit', defaultLimit)
            self.orderbooks[symbol] = self.order_book({}, limit)
            subscription['limit'] = limit
            self.spawn(self.fetch_order_book_snapshot, client, message, subscription)
        else:
            orderbook = self.orderbooks[symbol]
            prevNonce = self.safe_integer(orderbook, 'nonce')
            nonce = self.safe_integer(message, 'nonce')
            if nonce == (prevNonce + 1):
                self.handle_order_book_message(client, message, orderbook)
                client.resolve(orderbook, topic)

    async def fetch_order_book_snapshot(self, client, message, subscription):
        symbol = self.safe_string(message, 'symbol')
        market = self.market(symbol)
        messageHash = market['id'] + '@orderbook'
        try:
            defaultLimit = self.safe_integer(self.options, 'watchOrderBookLimit', 1000)
            limit = self.safe_integer(subscription, 'limit', defaultLimit)
            params = self.safe_dict(subscription, 'params')
            snapshot = await self.fetch_rest_order_book_safe(symbol, limit, params)
            if self.safe_value(self.orderbooks, symbol) is None:
                # if the orderbook is dropped before the snapshot is received
                return
            orderbook = self.orderbooks[symbol]
            orderbook.reset(snapshot)
            self.orderbooks[symbol] = orderbook
            client.resolve(orderbook, messageHash)
        except Exception as e:
            del client.subscriptions[messageHash]
            client.reject(e, messageHash)

    def handle_order_book_message(self, client: Client, message, orderbook):
        self.handle_deltas(orderbook['asks'], self.safe_value(message, 'asks', []))
        self.handle_deltas(orderbook['bids'], self.safe_value(message, 'bids', []))
        timestamp = self.safe_integer(message, 'timestamp')
        nonce = self.safe_integer(message, 'nonce')
        orderbook['timestamp'] = timestamp
        orderbook['datetime'] = self.iso8601(timestamp)
        orderbook['nonce'] = nonce
        return orderbook

    def handle_delta(self, bookside, delta):
        price = self.safe_float_2(delta, 'price', 0)
        amount = self.safe_float_2(delta, 'quantity', 1)
        bookside.store(price, amount)

    def handle_deltas(self, bookside, deltas):
        for i in range(0, len(deltas)):
            self.handle_delta(bookside, deltas[i])

    async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]:
        """

        https://testnet.aftermath.finance/docs/#/CCXT/service%3A%3Ahandlers%3A%3Accxt%3A%3Astream%3A%3Apositions

        watch all open positions
        :param str[]|None symbols: list of unified market symbols
        :param int [since]: the earliest time in ms to fetch positions for
        :param int [limit]: the maximum number of position structures to retrieve
        :param dict params: extra parameters specific to the exchange API endpoint
        :param int [params.accountNumber]: account number to query orders for, required
        :returns dict[]: a list of `position structure <https://docs.ccxt.com/en/latest/manual.html#position-structure>`
        """
        await self.load_markets()
        messageHashes = []
        symbols = self.market_symbols(symbols)
        if not self.is_empty(symbols):
            for i in range(0, len(symbols)):
                symbol = symbols[i]
                messageHashes.append('positions::' + symbol)
        else:
            messageHashes.append('positions')
        accountNumber = self.safe_number(params, 'accountNumber')
        request = {
            'accountNumber': accountNumber,
        }
        message = self.extend(request, params)
        suffix = 'positions'
        url = self.urls['api']['ws']['swap'] + '/' + suffix
        client = self.client(url)
        self.set_positions_cache(client, symbols, params)
        fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', True)
        awaitPositionsSnapshot = self.handle_option('watchPositions', 'awaitPositionsSnapshot', True)
        if fetchPositionsSnapshot and awaitPositionsSnapshot and self.positions is None:
            snapshot = await client.future('fetchPositionsSnapshot')
            return self.filter_by_symbols_since_limit(snapshot, symbols, since, limit, True)
        newPositions = await self.watch_public_multiple(suffix, messageHashes, message)
        if self.newUpdates:
            return newPositions
        return self.filter_by_symbols_since_limit(self.positions, symbols, since, limit, True)

    def set_positions_cache(self, client: Client, symbols: Strings = None, params: dict = {}):
        fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', False)
        if fetchPositionsSnapshot:
            messageHash = 'fetchPositionsSnapshot'
            if not (messageHash in client.futures):
                client.future(messageHash)
                self.spawn(self.load_positions_snapshot, client, messageHash, symbols, params)
        else:
            self.positions = ArrayCacheBySymbolBySide()

    async def load_positions_snapshot(self, client, messageHash, symbols, params):
        positions = await self.fetch_positions(symbols, params)
        self.positions = ArrayCacheBySymbolBySide()
        cache = self.positions
        for i in range(0, len(positions)):
            position = positions[i]
            cache.append(position)
        # don't remove the future from the .futures cache
        future = client.futures[messageHash]
        future.resolve(cache)
        client.resolve(cache, 'positions')

    def handle_positions(self, client, message):
        #
        # {
        #     "collateral": 0.1,
        #     "contractSize": 0.1,
        #     "contracts": 0.1,
        #     "datetime": "string",
        #     "entryPrice": 0.1,
        #     "id": "0x895037c09dd1025a136b5a5789c4ea2481176adf4c3b0c3521d5d2039bdfc3ba:123",
        #     "initialMargin": 0.1,
        #     "initialMarginPercentage": 0.1,
        #     "leverage": 0.1,
        #     "liquidationPrice": 0.1,
        #     "maintenanceMargin": 0.1,
        #     "maintenanceMarginPercentage": 0.1,
        #     "marginMode": "cross",
        #     "marginRatio": 0.1,
        #     "notional": 0.1,
        #     "side": "long",
        #     "symbol": "BTC/USD:USDC",
        #     "timestamp": 9007199254740991,
        #     "unrealizedPnl": 0.1
        # }
        #
        if self.positions is None:
            self.positions = ArrayCacheBySymbolBySide()
        cache = self.positions
        symbol = self.safe_string(message, 'symbol')
        market = self.safe_market(symbol)
        position = self.parse_position(message, market)
        cache.append(position)
        messageHash = 'positions::' + market['symbol']
        client.resolve(position, messageHash)
        client.resolve([position], 'positions')

    def handle_error_message(self, client: Client, message):
        #
        # User error: Expected Message::Text from client, got Ping(b\"\")
        #
        if isinstance(message, str):
            if message.find('error') >= 0:
                try:
                    feedback = self.id + ' ' + message
                    self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback)
                    raise ExchangeError(message)
                except Exception as error:
                    if isinstance(error, AuthenticationError):
                        messageHash = 'authenticated'
                        client.reject(error, messageHash)
                        if messageHash in client.subscriptions:
                            del client.subscriptions[messageHash]
                    else:
                        client.reject(error)
                    return True
        return False

    def handle_message(self, client: Client, message):
        if self.handle_error_message(client, message):
            return
        # methods: Dict = {
        #     'trade': self.handle_trade,
        # }
        if 'asks' in message:
            self.handle_order_book(client, message)
        elif 'notional' in message:
            self.handle_positions(client, message)
        else:
            self.handle_trade(client, message)
