# -*- 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, ArrayCacheBySymbolById
from ccxt.base.types import Any, Bool, Int, Order, OrderBook, Str, Ticker, 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
from ccxt.base.errors import UnsubscribeError


class derive(ccxt.async_support.derive):

    def describe(self) -> Any:
        return self.deep_extend(super(derive, self).describe(), {
            'has': {
                'ws': False,
                'watchBalance': False,
                'watchMyTrades': True,
                'watchOHLCV': False,
                'watchOrderBook': True,
                'watchOrders': True,
                'watchTicker': True,
                'watchTickers': False,
                'watchBidsAsks': False,
                'watchTrades': True,
                'watchTradesForSymbols': False,
                'watchPositions': False,
            },
            'urls': {
                'api': {
                    'ws': 'wss://api.lyra.finance/ws',
                },
                'test': {
                    'ws': 'wss://api-demo.lyra.finance/ws',
                },
            },
            'options': {
                'tradesLimit': 1000,
                'ordersLimit': 1000,
                'requestId': {},
            },
            'streaming': {
                'keepAlive': 9000,
            },
            'exceptions': {
                'ws': {
                    'exact': {},
                },
            },
        })

    def request_id(self, url):
        options = self.safe_value(self.options, 'requestId', {})
        previousValue = self.safe_integer(options, url, 0)
        newValue = self.sum(previousValue, 1)
        self.options['requestId'][url] = newValue
        return newValue

    async def watch_public(self, messageHash, message, subscription):
        url = self.urls['api']['ws']
        requestId = self.request_id(url)
        request = self.extend(message, {
            'id': requestId,
        })
        subscription = self.extend(subscription, {
            'id': requestId,
            'method': 'subscribe',
        })
        return await self.watch(url, messageHash, request, messageHash, subscription)

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

        https://docs.derive.xyz/reference/orderbook-instrument_name-group-depth

        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()
        if limit is None:
            limit = 10
        market = self.market(symbol)
        topic = 'orderbook.' + market['id'] + '.10.' + self.number_to_string(limit)
        request: dict = {
            'method': 'subscribe',
            'params': {
                'channels': [
                    topic,
                ],
            },
        }
        subscription: dict = {
            'name': topic,
            'symbol': symbol,
            'limit': limit,
            'params': params,
        }
        orderbook = await self.watch_public(topic, request, subscription)
        return orderbook.limit()

    def handle_order_book(self, client: Client, message):
        #
        # {
        #     method: 'subscription',
        #     params: {
        #       channel: 'orderbook.BTC-PERP.10.1',
        #       data: {
        #         timestamp: 1738331231506,
        #         instrument_name: 'BTC-PERP',
        #         publish_id: 628419,
        #         bids: [['104669', '40']],
        #         asks: [['104736', '40']]
        #       }
        #     }
        # }
        #
        params = self.safe_dict(message, 'params')
        data = self.safe_dict(params, 'data')
        marketId = self.safe_string(data, 'instrument_name')
        market = self.safe_market(marketId)
        symbol = market['symbol']
        topic = self.safe_string(params, 'channel')
        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)
        orderbook = self.orderbooks[symbol]
        timestamp = self.safe_integer(data, 'timestamp')
        snapshot = self.parse_order_book(data, symbol, timestamp, 'bids', 'asks')
        orderbook.reset(snapshot)
        client.resolve(orderbook, topic)

    async def watch_ticker(self, symbol: str, params={}) -> Ticker:
        """

        https://docs.derive.xyz/reference/ticker-instrument_name-interval

        watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
        :param str symbol: unified symbol of the market to fetch the ticker for
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
        """
        await self.load_markets()
        market = self.market(symbol)
        topic = 'ticker.' + market['id'] + '.100'
        request: dict = {
            'method': 'subscribe',
            'params': {
                'channels': [
                    topic,
                ],
            },
        }
        subscription: dict = {
            'name': topic,
            'symbol': symbol,
            'params': params,
        }
        return await self.watch_public(topic, request, subscription)

    def handle_ticker(self, client: Client, message):
        #
        # {
        #     method: 'subscription',
        #     params: {
        #       channel: 'ticker.BTC-PERP.100',
        #       data: {
        #         timestamp: 1738485104439,
        #         instrument_ticker: {
        #           instrument_type: 'perp',
        #           instrument_name: 'BTC-PERP',
        #           scheduled_activation: 1701840228,
        #           scheduled_deactivation: '9223372036854775807',
        #           is_active: True,
        #           tick_size: '0.1',
        #           minimum_amount: '0.01',
        #           maximum_amount: '10000',
        #           amount_step: '0.001',
        #           mark_price_fee_rate_cap: '0',
        #           maker_fee_rate: '0.0001',
        #           taker_fee_rate: '0.0003',
        #           base_fee: '0.1',
        #           base_currency: 'BTC',
        #           quote_currency: 'USD',
        #           option_details: null,
        #           perp_details: {
        #             index: 'BTC-USD',
        #             max_rate_per_hour: '0.004',
        #             min_rate_per_hour: '-0.004',
        #             static_interest_rate: '0.0000125',
        #             aggregate_funding: '10581.779418721074588722',
        #             funding_rate: '0.000024792239208858'
        #           },
        #           erc20_details: null,
        #           base_asset_address: '0xDBa83C0C654DB1cd914FA2710bA743e925B53086',
        #           base_asset_sub_id: '0',
        #           pro_rata_fraction: '0',
        #           fifo_min_allocation: '0',
        #           pro_rata_amount_step: '0.1',
        #           best_ask_amount: '0.131',
        #           best_ask_price: '99898.6',
        #           best_bid_amount: '0.056',
        #           best_bid_price: '99889.1',
        #           five_percent_bid_depth: '11.817',
        #           five_percent_ask_depth: '9.116',
        #           option_pricing: null,
        #           index_price: '99883.8',
        #           mark_price: '99897.52408421244763303548098',
        #           stats: {
        #             contract_volume: '92.395',
        #             num_trades: '2924',
        #             open_interest: '33.743468027373780786',
        #             high: '102320.4',
        #             low: '99064.3',
        #             percent_change: '-0.021356',
        #             usd_change: '-2178'
        #           },
        #           timestamp: 1738485165881,
        #           min_price: '97939.1',
        #           max_price: '101895.2'
        #         }
        #       }
        #     }
        # }
        #
        params = self.safe_dict(message, 'params')
        rawData = self.safe_dict(params, 'data')
        data = self.safe_dict(rawData, 'instrument_ticker')
        topic = self.safe_value(params, 'channel')
        ticker = self.parse_ticker(data)
        self.tickers[ticker['symbol']] = ticker
        client.resolve(ticker, topic)
        return message

    async def un_watch_order_book(self, symbol: str, params={}) -> Any:
        """
        unsubscribe from the orderbook channel
        :param str symbol: unified symbol of the market to fetch the order book for
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param int [params.limit]: orderbook limit, default is None
        :returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
        """
        await self.load_markets()
        limit = self.safe_integer(params, 'limit')
        if limit is None:
            limit = 10
        market = self.market(symbol)
        topic = 'orderbook.' + market['id'] + '.10.' + self.number_to_string(limit)
        messageHash = 'unwatch' + topic
        request: dict = {
            'method': 'unsubscribe',
            'params': {
                'channels': [
                    topic,
                ],
            },
        }
        subscription: dict = {
            'name': topic,
        }
        return await self.un_watch_public(messageHash, request, subscription)

    async def un_watch_trades(self, symbol: str, params={}) -> Any:
        """
        unsubscribe from the trades channel
        :param str symbol: unified symbol of the market to unwatch the trades for
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns any: status of the unwatch request
        """
        await self.load_markets()
        market = self.market(symbol)
        topic = 'trades.' + market['id']
        messageHah = 'unwatch' + topic
        request: dict = {
            'method': 'unsubscribe',
            'params': {
                'channels': [
                    topic,
                ],
            },
        }
        subscription: dict = {
            'name': topic,
        }
        return await self.un_watch_public(messageHah, request, subscription)

    async def un_watch_public(self, messageHash, message, subscription):
        url = self.urls['api']['ws']
        requestId = self.request_id(url)
        request = self.extend(message, {
            'id': requestId,
        })
        subscription = self.extend(subscription, {
            'id': requestId,
            'method': 'unsubscribe',
        })
        return await self.watch(url, messageHash, request, messageHash, subscription)

    def handle_order_book_un_subscription(self, client: Client, topic):
        parsedTopic = topic.split('.')
        marketId = self.safe_string(parsedTopic, 1)
        market = self.safe_market(marketId)
        symbol = market['symbol']
        if symbol in self.orderbooks:
            del self.orderbooks[symbol]
        if topic in client.subscriptions:
            del client.subscriptions[topic]
        error = UnsubscribeError(self.id + ' orderbook ' + symbol)
        client.reject(error, topic)
        client.resolve(error, 'unwatch' + topic)

    def handle_trades_un_subscription(self, client: Client, topic):
        parsedTopic = topic.split('.')
        marketId = self.safe_string(parsedTopic, 1)
        market = self.safe_market(marketId)
        symbol = market['symbol']
        if symbol in self.orderbooks:
            del self.trades[symbol]
        if topic in client.subscriptions:
            del client.subscriptions[topic]
        error = UnsubscribeError(self.id + ' trades ' + symbol)
        client.reject(error, topic)
        client.resolve(error, 'unwatch' + topic)

    def handle_un_subscribe(self, client: Client, message):
        #
        # {
        #     id: 1,
        #     result: {
        #       status: {'orderbook.BTC-PERP.10.10': 'ok'},
        #       remaining_subscriptions: []
        #     }
        # }
        #
        result = self.safe_dict(message, 'result')
        status = self.safe_dict(result, 'status')
        if status is not None:
            topics = list(status.keys())
            for i in range(0, len(topics)):
                topic = topics[i]
                if topic.find('orderbook') >= 0:
                    self.handle_order_book_un_subscription(client, topic)
                elif topic.find('trades') >= 0:
                    self.handle_trades_un_subscription(client, topic)
        return 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://docs.derive.xyz/reference/trades-instrument_name

        :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)
        topic = 'trades.' + market['id']
        request: dict = {
            'method': 'subscribe',
            'params': {
                'channels': [
                    topic,
                ],
            },
        }
        subscription: dict = {
            'name': topic,
            'symbol': symbol,
            'params': params,
        }
        trades = await self.watch_public(topic, request, subscription)
        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):
        #
        #
        params = self.safe_dict(message, 'params')
        data = self.safe_dict(params, 'data')
        topic = self.safe_value(params, 'channel')
        parsedTopic = topic.split('.')
        marketId = self.safe_string(parsedTopic, 1)
        market = self.safe_market(marketId)
        symbol = market['symbol']
        tradesArray = self.safe_value(self.trades, symbol)
        if tradesArray is None:
            limit = self.safe_integer(self.options, 'tradesLimit', 1000)
            tradesArray = ArrayCache(limit)
        for i in range(0, len(data)):
            trade = self.parse_trade(data[i])
            tradesArray.append(trade)
        self.trades[symbol] = tradesArray
        client.resolve(tradesArray, topic)

    async def authenticate(self, params={}):
        self.check_required_credentials()
        url = self.urls['api']['ws']
        client = self.client(url)
        messageHash = 'authenticated'
        future = client.reusableFuture(messageHash)
        authenticated = self.safe_value(client.subscriptions, messageHash)
        if authenticated is None:
            requestId = self.request_id(url)
            now = str(self.milliseconds())
            signature = self.signMessage(now, self.privateKey)
            deriveWalletAddress = self.safe_string(self.options, 'deriveWalletAddress')
            request: dict = {
                'id': requestId,
                'method': 'public/login',
                'params': {
                    'wallet': deriveWalletAddress,
                    'timestamp': now,
                    'signature': signature,
                },
            }
            # subscription: Dict = {
            #     'name': topic,
            #     'symbol': symbol,
            #     'params': params,
            # }
            message = self.extend(request, params)
            self.watch(url, messageHash, message, messageHash, message)
        return await future

    async def watch_private(self, messageHash, message, subscription):
        await self.authenticate()
        url = self.urls['api']['ws']
        requestId = self.request_id(url)
        request = self.extend(message, {
            'id': requestId,
        })
        subscription = self.extend(subscription, {
            'id': requestId,
            'method': 'subscribe',
        })
        return await self.watch(url, messageHash, request, messageHash, subscription)

    async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
        """

        https://docs.derive.xyz/reference/subaccount_id-orders

        watches information on multiple orders made by the user
        :param str symbol: unified market symbol of the market orders were made in
        :param int [since]: the earliest time in ms to fetch orders for
        :param int [limit]: the maximum number of order structures to retrieve
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.subaccount_id]: *required* the subaccount id
        :returns dict[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
        """
        await self.load_markets()
        subaccountId = None
        subaccountId, params = self.handleDeriveSubaccountId('watchOrders', params)
        topic = self.number_to_string(subaccountId) + '.orders'
        messageHash = topic
        if symbol is not None:
            market = self.market(symbol)
            symbol = market['symbol']
            messageHash += ':' + symbol
        request: dict = {
            'method': 'subscribe',
            'params': {
                'channels': [
                    topic,
                ],
            },
        }
        subscription: dict = {
            'name': topic,
            'params': params,
        }
        message = self.extend(request, params)
        orders = await self.watch_private(messageHash, message, subscription)
        if self.newUpdates:
            limit = orders.getLimit(symbol, limit)
        return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True)

    def handle_order(self, client: Client, message):
        #
        # {
        #     method: 'subscription',
        #     params: {
        #         channel: '130837.orders',
        #         data: [
        #             {
        #                 subaccount_id: 130837,
        #                 order_id: '1f44c564-5658-4b69-b8c4-4019924207d5',
        #                 instrument_name: 'BTC-PERP',
        #                 direction: 'buy',
        #                 label: 'test1234',
        #                 quote_id: null,
        #                 creation_timestamp: 1738578974146,
        #                 last_update_timestamp: 1738578974146,
        #                 limit_price: '10000',
        #                 amount: '0.01',
        #                 filled_amount: '0',
        #                 average_price: '0',
        #                 order_fee: '0',
        #                 order_type: 'limit',
        #                 time_in_force: 'post_only',
        #                 order_status: 'untriggered',
        #                 max_fee: '219',
        #                 signature_expiry_sec: 1746354973,
        #                 nonce: 1738578973570,
        #                 signer: '0x30CB7B06AdD6749BbE146A6827502B8f2a79269A',
        #                 signature: '0xc6927095f74a0d3b1aeef8c0579d120056530479f806e9d2e6616df742a8934c69046361beae833b32b25c0145e318438d7d1624bb835add956f63aa37192f571c',
        #                 cancel_reason: '',
        #                 mmp: False,
        #                 is_transfer: False,
        #                 replaced_order_id: null,
        #                 trigger_type: 'stoploss',
        #                 trigger_price_type: 'mark',
        #                 trigger_price: '102800',
        #                 trigger_reject_message: null
        #             }
        #         ]
        #     }
        # }
        #
        params = self.safe_dict(message, 'params')
        topic = self.safe_string(params, 'channel')
        rawOrders = self.safe_list(params, 'data')
        for i in range(0, len(rawOrders)):
            data = rawOrders[i]
            parsed = self.parse_order(data)
            symbol = self.safe_string(parsed, 'symbol')
            orderId = self.safe_string(parsed, 'id')
            if symbol is not None:
                if self.orders is None:
                    limit = self.safe_integer(self.options, 'ordersLimit', 1000)
                    self.orders = ArrayCacheBySymbolById(limit)
                cachedOrders = self.orders
                orders = self.safe_value(cachedOrders.hashmap, symbol, {})
                order = self.safe_value(orders, orderId)
                if order is not None:
                    fee = self.safe_value(order, 'fee')
                    if fee is not None:
                        parsed['fee'] = fee
                    fees = self.safe_value(order, 'fees')
                    if fees is not None:
                        parsed['fees'] = fees
                    parsed['trades'] = self.safe_value(order, 'trades')
                    parsed['timestamp'] = self.safe_integer(order, 'timestamp')
                    parsed['datetime'] = self.safe_string(order, 'datetime')
                cachedOrders.append(parsed)
                messageHashSymbol = topic + ':' + symbol
                client.resolve(self.orders, messageHashSymbol)
        client.resolve(self.orders, topic)

    async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
        """

        https://docs.derive.xyz/reference/subaccount_id-trades

        watches information on multiple trades made by the user
        :param str symbol: unified market symbol of the market orders were made in
        :param int [since]: the earliest time in ms to fetch orders for
        :param int [limit]: the maximum number of order structures to retrieve
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.subaccount_id]: *required* the subaccount id
        :returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`
        """
        await self.load_markets()
        subaccountId = None
        subaccountId, params = self.handleDeriveSubaccountId('watchMyTrades', params)
        topic = self.number_to_string(subaccountId) + '.trades'
        messageHash = topic
        if symbol is not None:
            market = self.market(symbol)
            symbol = market['symbol']
            messageHash += ':' + symbol
        request: dict = {
            'method': 'subscribe',
            'params': {
                'channels': [
                    topic,
                ],
            },
        }
        subscription: dict = {
            'name': topic,
            'params': params,
        }
        message = self.extend(request, params)
        trades = await self.watch_private(messageHash, message, subscription)
        if self.newUpdates:
            limit = trades.getLimit(symbol, limit)
        return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True)

    def handle_my_trade(self, client: Client, message):
        #
        #
        myTrades = self.myTrades
        if myTrades is None:
            limit = self.safe_integer(self.options, 'tradesLimit', 1000)
            myTrades = ArrayCacheBySymbolById(limit)
        params = self.safe_dict(message, 'params')
        topic = self.safe_string(params, 'channel')
        rawTrades = self.safe_list(params, 'data')
        for i in range(0, len(rawTrades)):
            trade = self.parse_trade(message)
            myTrades.append(trade)
            client.resolve(myTrades, topic)
            messageHash = topic + trade['symbol']
            client.resolve(myTrades, messageHash)

    def handle_error_message(self, client: Client, message) -> Bool:
        #
        # {
        #     id: '690c6276-0fc6-4121-aafa-f28bf5adedcb',
        #     error: {code: -32600, message: 'Invalid Request'}
        # }
        #
        if not ('error' in message):
            return False
        errorMessage = self.safe_dict(message, 'error')
        errorCode = self.safe_string(errorMessage, 'code')
        try:
            if errorCode is not None:
                feedback = self.id + ' ' + self.json(message)
                self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback)
                raise ExchangeError(feedback)
            return False
        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

    def handle_message(self, client: Client, message):
        if self.handle_error_message(client, message):
            return
        methods: dict = {
            'orderbook': self.handle_order_book,
            'ticker': self.handle_ticker,
            'trades': self.handle_trade,
            'orders': self.handle_order,
            'mytrades': self.handle_my_trade,
        }
        event = None
        params = self.safe_dict(message, 'params')
        if params is not None:
            channel = self.safe_string(params, 'channel')
            if channel is not None:
                parsedChannel = channel.split('.')
                if (channel.find('orders') >= 0) or channel.find('trades') > 0:
                    event = self.safe_string(parsedChannel, 1)
                    # {subaccounr_id}.trades
                    if event == 'trades':
                        event = 'mytrades'
                else:
                    event = self.safe_string(parsedChannel, 0)
        method = self.safe_value(methods, event)
        if method is not None:
            method(client, message)
            return
        if 'id' in message:
            id = self.safe_string(message, 'id')
            subscriptionsById = self.index_by(client.subscriptions, 'id')
            subscription = self.safe_value(subscriptionsById, id, {})
            if 'method' in subscription:
                if subscription['method'] == 'public/login':
                    self.handle_auth(client, message)
                elif subscription['method'] == 'unsubscribe':
                    self.handle_un_subscribe(client, message)
                # could handleSubscribe

    def handle_auth(self, client: Client, message):
        #
        # {
        #     id: 1,
        #     result: [130837]
        # }
        #
        messageHash = 'authenticated'
        ids = self.safe_list(message, 'result')
        if len(ids) > 0:
            # client.resolve(message, messageHash)
            future = self.safe_value(client.futures, 'authenticated')
            future.resolve(True)
        else:
            error = AuthenticationError(self.json(message))
            client.reject(error, messageHash)
            # allows further authentication attempts
            if messageHash in client.subscriptions:
                del client.subscriptions['authenticated']
