# -*- 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

from ccxt.async_support.base.exchange import Exchange
import base64
import hashlib
import math
from ccxt.base.errors import ExchangeError
from ccxt.base.errors import AuthenticationError
from ccxt.base.errors import PermissionDenied
from ccxt.base.errors import AddressPending
from ccxt.base.errors import NotSupported


class buda (Exchange):

    def describe(self):
        return self.deep_extend(super(buda, self).describe(), {
            'id': 'buda',
            'name': 'Buda',
            'countries': ['AR', 'CL', 'CO', 'PE'],
            'rateLimit': 1000,
            'version': 'v2',
            'has': {
                'CORS': False,
                'createDepositAddress': True,
                'fetchClosedOrders': True,
                'fetchCurrencies': True,
                'fetchDepositAddress': True,
                'fetchDeposits': True,
                'fetchFundingFees': True,
                'fetchMyTrades': False,
                'fetchOHLCV': True,
                'fetchOpenOrders': True,
                'fetchOrder': True,
                'fetchOrders': True,
                'fetchTrades': True,
                'fetchWithdrawals': True,
                'withdraw': True,
            },
            'urls': {
                'logo': 'https://user-images.githubusercontent.com/1294454/47380619-8a029200-d706-11e8-91e0-8a391fe48de3.jpg',
                'api': 'https://www.buda.com/api',
                'www': 'https://www.buda.com',
                'doc': 'https://api.buda.com',
                'fees': 'https://www.buda.com/comisiones',
            },
            'status': {
                'status': 'error',
                'updated': None,
                'eta': None,
                'url': None,
            },
            'api': {
                'public': {
                    'get': [
                        'pairs',
                        'markets',
                        'currencies',
                        'markets/{market}',
                        'markets/{market}/ticker',
                        'markets/{market}/volume',
                        'markets/{market}/order_book',
                        'markets/{market}/trades',
                        'currencies/{currency}/fees/deposit',
                        'currencies/{currency}/fees/withdrawal',
                        'tv/history',
                    ],
                    'post': [
                        'markets/{market}/quotations',
                    ],
                },
                'private': {
                    'get': [
                        'balances',
                        'balances/{currency}',
                        'currencies/{currency}/balances',
                        'orders',
                        'orders/{id}',
                        'markets/{market}/orders',
                        'deposits',
                        'currencies/{currency}/deposits',
                        'withdrawals',
                        'currencies/{currency}/withdrawals',
                        'currencies/{currency}/receive_addresses',
                        'currencies/{currency}/receive_addresses/{id}',
                    ],
                    'post': [
                        'markets/{market}/orders',
                        'currencies/{currency}/deposits',
                        'currencies/{currency}/withdrawals',
                        'currencies/{currency}/simulated_withdrawals',
                        'currencies/{currency}/receive_addresses',
                    ],
                    'put': [
                        'orders/{id}',
                    ],
                },
            },
            'timeframes': {
                '1m': '1',
                '5m': '5',
                '30m': '30',
                '1h': '60',
                '2h': '120',
                '1d': 'D',
                '1w': 'W',
            },
            'fees': {
                'trading': {
                    'tierBased': True,
                    'percentage': True,
                    'taker': 0.008,  # 0.8%
                    'maker': 0.004,  # 0.4%
                    'tiers': {
                        'taker': [
                            [0, 0.008],  # 0.8%
                            [2000, 0.007],  # 0.7%
                            [20000, 0.006],  # 0.6%
                            [100000, 0.005],  # 0.5%
                            [500000, 0.004],  # 0.4%
                            [2500000, 0.003],  # 0.3%
                            [12500000, 0.002],  # 0.2%
                        ],
                        'maker': [
                            [0, 0.004],  # 0.4%
                            [2000, 0.0035],  # 0.35%
                            [20000, 0.003],  # 0.3%
                            [100000, 0.0025],  # 0.25%
                            [500000, 0.002],  # 0.2%
                            [2500000, 0.0015],  # 0.15%
                            [12500000, 0.001],  # 0.1%
                        ],
                    },
                },
            },
            'exceptions': {
                'not_authorized': AuthenticationError,  # {message: 'Invalid credentials', code: 'not_authorized'}
                'forbidden': PermissionDenied,  # {message: 'You dont have access to self resource', code: 'forbidden'}
                'invalid_record': ExchangeError,  # {message: 'Validation Failed', code: 'invalid_record', errors: []}
                'not_found': ExchangeError,  # {message: 'Not found', code: 'not_found'}
                'parameter_missing': ExchangeError,  # {message: 'Parameter missing', code: 'parameter_missing'}
                'bad_parameter': ExchangeError,  # {message: 'Bad Parameter format', code: 'bad_parameter'}
            },
        })

    async def fetch_currency_info(self, currency, currencies=None):
        if not currencies:
            response = await self.publicGetCurrencies()
            currencies = self.safe_value(response, 'currencies')
        for i in range(0, len(currencies)):
            currencyInfo = currencies[i]
            if currencyInfo['id'] == currency:
                return currencyInfo
        return None

    async def fetch_markets(self, params={}):
        marketsResponse = await self.publicGetMarkets(params)
        markets = self.safe_value(marketsResponse, 'markets')
        currenciesResponse = await self.publicGetCurrencies()
        currencies = self.safe_value(currenciesResponse, 'currencies')
        result = []
        for i in range(0, len(markets)):
            market = markets[i]
            id = self.safe_string(market, 'id')
            baseId = self.safe_string(market, 'base_currency')
            quoteId = self.safe_string(market, 'quote_currency')
            base = self.safe_currency_code(baseId)
            quote = self.safe_currency_code(quoteId)
            baseInfo = await self.fetch_currency_info(baseId, currencies)
            quoteInfo = await self.fetch_currency_info(quoteId, currencies)
            symbol = base + '/' + quote
            precision = {
                'amount': baseInfo['input_decimals'],
                'price': quoteInfo['input_decimals'],
            }
            limits = {
                'amount': {
                    'min': float(market['minimum_order_amount'][0]),
                    'max': None,
                },
                'price': {
                    'min': math.pow(10, -precision['price']),
                    'max': None,
                },
            }
            limits['cost'] = {
                'min': limits['amount']['min'] * limits['price']['min'],
                'max': None,
            }
            result.append({
                'id': id,
                'symbol': symbol,
                'base': base,
                'quote': quote,
                'baseId': baseId,
                'quoteId': quoteId,
                'active': True,
                'precision': precision,
                'limits': limits,
                'info': market,
            })
        return result

    async def fetch_currencies(self, params={}):
        response = await self.publicGetCurrencies()
        currencies = response['currencies']
        result = {}
        for i in range(0, len(currencies)):
            currency = currencies[i]
            if not currency['managed']:
                continue
            id = self.safe_string(currency, 'id')
            code = self.safe_currency_code(id)
            precision = self.safe_float(currency, 'input_decimals')
            minimum = math.pow(10, -precision)
            result[code] = {
                'id': id,
                'code': code,
                'info': currency,
                'name': None,
                'active': True,
                'fee': None,
                'precision': precision,
                'limits': {
                    'amount': {
                        'min': minimum,
                        'max': None,
                    },
                    'price': {
                        'min': minimum,
                        'max': None,
                    },
                    'cost': {
                        'min': None,
                        'max': None,
                    },
                    'deposit': {
                        'min': float(currency['deposit_minimum'][0]),
                        'max': None,
                    },
                    'withdraw': {
                        'min': float(currency['withdrawal_minimum'][0]),
                    },
                },
            }
        return result

    async def fetch_funding_fees(self, codes=None, params={}):
        #  by default it will try load withdrawal fees of all currencies(with separate requests)
        #  however if you define codes = ['ETH', 'BTC'] in args it will only load those
        await self.load_markets()
        withdrawFees = {}
        depositFees = {}
        info = {}
        if codes is None:
            codes = list(self.currencies.keys())
        for i in range(0, len(codes)):
            code = codes[i]
            currency = self.currency(code)
            request = {'currency': currency['id']}
            withdrawResponse = await self.publicGetCurrenciesCurrencyFeesWithdrawal(request)
            depositResponse = await self.publicGetCurrenciesCurrencyFeesDeposit(request)
            withdrawFees[code] = self.parse_funding_fee(withdrawResponse['fee'])
            depositFees[code] = self.parse_funding_fee(depositResponse['fee'])
            info[code] = {
                'withdraw': withdrawResponse,
                'deposit': depositResponse,
            }
        return {
            'withdraw': withdrawFees,
            'deposit': depositFees,
            'info': info,
        }

    def parse_funding_fee(self, fee, type=None):
        if type is None:
            type = fee['name']
        if type == 'withdrawal':
            type = 'withdraw'
        return {
            'type': type,
            'currency': fee['base'][1],
            'rate': fee['percent'],
            'cost': float(fee['base'][0]),
        }

    async def fetch_ticker(self, symbol, params={}):
        await self.load_markets()
        market = self.market(symbol)
        request = {
            'market': market['id'],
        }
        response = await self.publicGetMarketsMarketTicker(self.extend(request, params))
        ticker = self.safe_value(response, 'ticker')
        return self.parse_ticker(ticker, market)

    def parse_ticker(self, ticker, market=None):
        timestamp = self.milliseconds()
        symbol = None
        if market is not None:
            symbol = market['symbol']
        last = float(ticker['last_price'][0])
        percentage = float(ticker['price_variation_24h'])
        open = float(self.price_to_precision(symbol, last / (percentage + 1)))
        change = last - open
        average = self.sum(last, open) / 2
        return {
            'symbol': symbol,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'high': None,
            'low': None,
            'bid': float(ticker['max_bid'][0]),
            'bidVolume': None,
            'ask': float(ticker['min_ask'][0]),
            'askVolume': None,
            'vwap': None,
            'open': open,
            'close': last,
            'last': last,
            'previousClose': open,
            'change': change,
            'percentage': percentage * 100,
            'average': average,
            'baseVolume': float(ticker['volume'][0]),
            'quoteVolume': None,
            'info': ticker,
        }

    async def fetch_trades(self, symbol, since=None, limit=None, params={}):
        await self.load_markets()
        market = self.market(symbol)
        request = {
            'market': market['id'],
        }
        # the since argument works backwards – returns trades up to the specified timestamp
        # therefore not implemented here
        # the method is still available for users to be able to traverse backwards in time
        # by using the timestamp from the first received trade upon each iteration
        if limit is not None:
            request['limit'] = limit  # 50 max
        response = await self.publicGetMarketsMarketTrades(self.extend(request, params))
        #
        #     {trades: {     market_id:   "ETH-BTC",
        #                      timestamp:    null,
        #                 last_timestamp:   "1536901277302",
        #                        entries: [["1540077456791", "0.0063767", "0.03", "sell", 479842],
        #                                   ["1539916642772", "0.01888263", "0.03019563", "sell", 479438],
        #                                   ["1539834081787", "0.023718648", "0.031001", "sell", 479069],
        #                                   ...]
        #
        return self.parse_trades(response['trades']['entries'], market, since, limit)

    def parse_trade(self, trade, market=None):
        #
        # fetchTrades(public)
        #  ["1540077456791", "0.0063767", "0.03", "sell", 479842]
        #
        timestamp = None
        side = None
        type = None
        price = None
        amount = None
        id = None
        order = None
        fee = None
        symbol = None
        cost = None
        if market:
            symbol = market['symbol']
        if isinstance(trade, list):
            timestamp = int(trade[0])
            price = float(trade[1])
            amount = float(trade[2])
            cost = price * amount
            side = trade[3]
            id = str(trade[4])
        return {
            'id': id,
            'order': order,
            'info': trade,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'symbol': symbol,
            'type': type,
            'side': side,
            'takerOrMaker': None,
            'price': price,
            'amount': amount,
            'cost': cost,
            'fee': fee,
        }

    async def fetch_order_book(self, symbol, limit=None, params={}):
        await self.load_markets()
        market = self.market(symbol)
        request = {
            'market': market['id'],
        }
        response = await self.publicGetMarketsMarketOrderBook(self.extend(request, params))
        orderbook = self.safe_value(response, 'order_book')
        return self.parse_order_book(orderbook)

    async def fetch_ohlcv(self, symbol, timeframe='1m', since=None, limit=None, params={}):
        await self.load_markets()
        market = self.market(symbol)
        if since is None:
            since = self.milliseconds() - 86400000
        request = {
            'symbol': market['id'],
            'resolution': self.timeframes[timeframe],
            'from': since / 1000,
            'to': self.seconds(),
        }
        response = await self.publicGetTvHistory(self.extend(request, params))
        return self.parse_trading_view_ohlcv(response, market, timeframe, since, limit)

    async def fetch_balance(self, params={}):
        await self.load_markets()
        response = await self.privateGetBalances(params)
        result = {'info': response}
        balances = self.safe_value(response, 'balances')
        for i in range(0, len(balances)):
            balance = balances[i]
            currencyId = self.safe_string(balance, 'id')
            code = self.safe_currency_code(currencyId)
            account = self.account()
            account['free'] = float(balance['available_amount'][0])
            account['total'] = float(balance['amount'][0])
            result[code] = account
        return self.parse_balance(result)

    async def fetch_order(self, id, symbol=None, params={}):
        await self.load_markets()
        request = {
            'id': int(id),
        }
        response = await self.privateGetOrdersId(self.extend(request, params))
        order = self.safe_value(response, 'order')
        return self.parse_order(order)

    async def fetch_orders(self, symbol=None, since=None, limit=None, params={}):
        await self.load_markets()
        market = None
        if symbol is not None:
            market = self.market(symbol)
        request = {
            'market': market['id'],
            'per': limit,
        }
        response = await self.privateGetMarketsMarketOrders(self.extend(request, params))
        orders = self.safe_value(response, 'orders')
        return self.parse_orders(orders, market, since, limit)

    async def fetch_open_orders(self, symbol=None, since=None, limit=None, params={}):
        request = {
            'state': 'pending',
        }
        return await self.fetch_orders(symbol, since, limit, self.extend(request, params))

    async def fetch_closed_orders(self, symbol=None, since=None, limit=None, params={}):
        request = {
            'state': 'traded',
        }
        return await self.fetch_orders(symbol, since, limit, self.extend(request, params))

    async def create_order(self, symbol, type, side, amount, price=None, params={}):
        await self.load_markets()
        side = 'Bid' if (side == 'buy') else 'Ask'
        request = {
            'market': self.market_id(symbol),
            'price_type': type,
            'type': side,
            'amount': self.amount_to_precision(symbol, amount),
        }
        if type == 'limit':
            request['limit'] = self.price_to_precision(symbol, price)
        response = await self.privatePostMarketsMarketOrders(self.extend(request, params))
        order = self.safe_value(response, 'order')
        return self.parse_order(order)

    async def cancel_order(self, id, symbol=None, params={}):
        await self.load_markets()
        request = {
            'id': int(id),
            'state': 'canceling',
        }
        response = await self.privatePutOrdersId(self.extend(request, params))
        order = self.safe_value(response, 'order')
        return self.parse_order(order)

    def parse_order_status(self, status):
        statuses = {
            'traded': 'closed',
            'received': 'open',
            'canceling': 'canceled',
        }
        return self.safe_string(statuses, status, status)

    def parse_order(self, order, market=None):
        id = self.safe_string(order, 'id')
        timestamp = self.parse8601(self.safe_string(order, 'created_at'))
        symbol = None
        if market is None:
            marketId = order['market_id']
            if marketId in self.markets_by_id:
                market = self.markets_by_id[marketId]
        if market is not None:
            symbol = market['symbol']
        type = self.safe_string(order, 'price_type')
        side = self.safe_string_lower(order, 'type')
        status = self.parse_order_status(self.safe_string(order, 'state'))
        amount = float(order['original_amount'][0])
        remaining = float(order['amount'][0])
        filled = float(order['traded_amount'][0])
        cost = float(order['total_exchanged'][0])
        price = self.safe_float(order, 'limit')
        if price is not None:
            price = float(price[0])
        if cost > 0 and filled > 0:
            price = self.price_to_precision(symbol, cost / filled)
        fee = {
            'cost': float(order['paid_fee'][0]),
            'currency': order['paid_fee'][1],
        }
        return {
            'id': id,
            'datetime': self.iso8601(timestamp),
            'timestamp': timestamp,
            'lastTradeTimestamp': None,
            'status': status,
            'symbol': symbol,
            'type': type,
            'side': side,
            'price': price,
            'cost': cost,
            'amount': amount,
            'filled': filled,
            'remaining': remaining,
            'trades': None,
            'fee': fee,
            'info': order,
        }

    def is_fiat(self, code):
        fiats = {
            'ARS': True,
            'CLP': True,
            'COP': True,
            'PEN': True,
        }
        return self.safe_value(fiats, code, False)

    async def fetch_deposit_address(self, code, params={}):
        await self.load_markets()
        currency = self.currency(code)
        if self.is_fiat(code):
            raise NotSupported(self.id + ' fetchDepositAddress() for fiat ' + code + ' is not supported')
        request = {
            'currency': currency['id'],
        }
        response = await self.privateGetCurrenciesCurrencyReceiveAddresses(self.extend(request, params))
        receiveAddresses = self.safe_value(response, 'receive_addresses')
        addressPool = []
        for i in range(1, len(receiveAddresses)):
            receiveAddress = receiveAddresses[i]
            if receiveAddress['ready']:
                address = receiveAddress['address']
                self.check_address(address)
                addressPool.append(address)
        addressPoolLength = len(addressPool)
        if addressPoolLength < 1:
            raise AddressPending(self.id + ': there are no addresses ready for receiving ' + code + ', retry again later)')
        address = addressPool[0]
        return {
            'currency': code,
            'address': address,
            'tag': None,
            'info': receiveAddresses,
        }

    async def create_deposit_address(self, code, params={}):
        await self.load_markets()
        currency = self.currency(code)
        if self.is_fiat(code):
            raise NotSupported(self.id + ': fiat fetchDepositAddress() for ' + code + ' is not supported')
        request = {
            'currency': currency['id'],
        }
        response = await self.privatePostCurrenciesCurrencyReceiveAddresses(self.extend(request, params))
        address = self.safe_string(response['receive_address'], 'address')  # the creation is async and returns a null address, returns only the id
        return {
            'currency': code,
            'address': address,
            'tag': None,
            'info': response,
        }

    def parse_transaction_status(self, status):
        statuses = {
            'rejected': 'failed',
            'confirmed': 'ok',
            'anulled': 'canceled',
            'retained': 'canceled',
            'pending_confirmation': 'pending',
        }
        return self.safe_string(statuses, status, status)

    def parse_transaction(self, transaction, currency=None):
        id = self.safe_string(transaction, 'id')
        timestamp = self.parse8601(self.safe_string(transaction, 'created_at'))
        currencyId = self.safe_string(transaction, 'currency')
        code = self.safe_currency_code(currencyId, currency)
        amount = float(transaction['amount'][0])
        fee = float(transaction['fee'][0])
        feeCurrency = transaction['fee'][1]
        status = self.parse_transaction_status(self.safe_string(transaction, 'state'))
        type = 'deposit' if ('deposit_data' in list(transaction.keys())) else 'withdrawal'
        data = self.safe_value(transaction, type + '_data', {})
        address = self.safe_value(data, 'target_address')
        txid = self.safe_string(data, 'tx_hash')
        updated = self.parse8601(self.safe_string(data, 'updated_at'))
        return {
            'info': transaction,
            'id': id,
            'txid': txid,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'address': address,
            'type': type,
            'amount': amount,
            'currency': code,
            'status': status,
            'updated': updated,
            'fee': {
                'cost': fee,
                'rate': feeCurrency,
            },
        }

    async def fetch_deposits(self, code=None, since=None, limit=None, params={}):
        await self.load_markets()
        if code is None:
            raise ExchangeError(self.id + ': fetchDeposits() requires a currency code argument')
        currency = self.currency(code)
        request = {
            'currency': currency['id'],
            'per': limit,
        }
        response = await self.privateGetCurrenciesCurrencyDeposits(self.extend(request, params))
        deposits = self.safe_value(response, 'deposits')
        return self.parseTransactions(deposits, currency, since, limit)

    async def fetch_withdrawals(self, code=None, since=None, limit=None, params={}):
        await self.load_markets()
        if code is None:
            raise ExchangeError(self.id + ': fetchDeposits() requires a currency code argument')
        currency = self.currency(code)
        request = {
            'currency': currency['id'],
            'per': limit,
        }
        response = await self.privateGetCurrenciesCurrencyWithdrawals(self.extend(request, params))
        withdrawals = self.safe_value(response, 'withdrawals')
        return self.parseTransactions(withdrawals, currency, since, limit)

    async def withdraw(self, code, amount, address, tag=None, params={}):
        self.check_address(address)
        await self.load_markets()
        currency = self.currency(code)
        request = {
            'currency': currency['id'],
            'amount': amount,
            'withdrawal_data': {
                'target_address': address,
            },
        }
        response = await self.privatePostCurrenciesCurrencyWithdrawals(self.extend(request, params))
        withdrawal = self.safe_value(response, 'withdrawal')
        return self.parse_transaction(withdrawal)

    def nonce(self):
        return self.microseconds()

    def sign(self, path, api='public', method='GET', params={}, headers=None, body=None):
        request = self.implode_params(path, params)
        query = self.omit(params, self.extract_params(path))
        if query:
            if method == 'GET':
                request += '?' + self.urlencode(query)
            else:
                body = self.json(query)
        url = self.urls['api'] + '/' + self.version + '/' + request
        if api == 'private':
            self.check_required_credentials()
            nonce = str(self.nonce())
            components = [method, '/api/' + self.version + '/' + request]
            if body:
                base64Body = base64.b64encode(self.encode(body))
                components.append(self.decode(base64Body))
            components.append(nonce)
            message = ' '.join(components)
            signature = self.hmac(self.encode(message), self.encode(self.secret), hashlib.sha384)
            headers = {
                'X-SBTC-APIKEY': self.apiKey,
                'X-SBTC-SIGNATURE': signature,
                'X-SBTC-NONCE': nonce,
                'Content-Type': 'application/json',
            }
        return {'url': url, 'method': method, 'body': body, 'headers': headers}

    def handle_errors(self, code, reason, url, method, headers, body, response, requestHeaders, requestBody):
        if response is None:
            return  # fallback to default error handler
        if code >= 400:
            errorCode = self.safe_string(response, 'code')
            message = self.safe_string(response, 'message', body)
            feedback = self.id + ' ' + message
            exceptions = self.exceptions
            if errorCode is not None:
                if errorCode in exceptions:
                    raise exceptions[errorCode](feedback)
                else:
                    raise ExchangeError(feedback)
