from __future__ import print_function

from collections import defaultdict

from moneywagon import ExchangeUniverse
from moneywagon.services import Service

def all_balances(currency, services=None, verbose=False, timeout=None):
    """
    Get balances for passed in currency for all exchanges.
    """
    balances = {}
    if not services:
        services = [
            x(verbose=verbose, timeout=timeout)
            for x in ExchangeUniverse.get_authenticated_services()
        ]

    for e in services:
        try:
            balances[e] = e.get_exchange_balance(currency)
        except NotImplementedError:
            if verbose:
                print(e.name, "balance not implemented")
        except Exception as exc:
            if verbose:
                print(e.name, "failed:", exc.__class__.__name__, str(exc))

    return balances

def total_exchange_balances(services=None, verbose=None, timeout=None):
    """
    Returns all balances for all currencies for all exchanges
    """
    balances = defaultdict(lambda: 0)
    if not services:
        services = [
            x(verbose=verbose, timeout=timeout)
            for x in ExchangeUniverse.get_authenticated_services()
        ]

    for e in services:
        try:
            more_balances = e.get_total_exchange_balances()
            for code, bal in more_balances.items():
                balances[code] += bal
        except NotImplementedError:
            if verbose:
                print(e.name, "total balance not implemented")
        except Exception as exc:
            if verbose:
                print(e.name, "failed:", exc.__class__.__name__, str(exc))

    return balances

def transfer_balance_on_exchange(currency, from_ex, to_ex, percent=None, amount=None, verbose=False):
    if not isinstance(from_ex, Service): # if class passed in, instantiate
        from_ex = from_ex(verbose=verbose)
    if not isinstance(to_ex, Service):
        to_ex = to_ex(verbose=verbose)

    if not amount and not percent:
        raise Exception("One of `amount` or `percent` required.")
    if percent and amount:
        raise Exception("Either `amount` or `percent`, not both")

    if percent:
        balance = from_ex.get_exchange_balance(currency)
        amount = balance * (percent / 100)
        if verbose:
            print("Sending %.2f%% of %.8f which is %.8f" % (percent, balance, amount))
    to_address = to_ex.get_deposit_address(currency)
    if verbose:
        print("to address: %s" % to_address)
    return from_ex.initiate_withdraw(currency, amount, to_address)


class MultiOrderBook(object):

    def __init__(self, services=None, verbose=False):
        from moneywagon import EXCHANGE_SERVICES
        self.services = services or [x(verbose=verbose) for x in EXCHANGE_SERVICES]
        self.got_orderbook_services = []
        self.verbose = verbose

    def get(self, crypto, fiat, trim=False, trim_crypto=None, trim_fiat=None):
        services = None
        if trim:
            trim_crypto, trim_fiat = True, True
        if trim_fiat is True:
            trim_fiat = all_balances(fiat, services=self.services)
        if trim_crypto is True:
            if trim_fiat:
                services = trim_fiat.keys()
            else:
                services = self.services
            trim_crypto = all_balances(crypto, services=services)

        if trim_crypto or trim_fiat:
            services = self._services_from_balances(trim_crypto, trim_fiat)
        elif not services:
            services = self.services

        combined = {'bids': [], 'asks': []}
        for service in services:
            try:
                book = service.get_orderbook(crypto, fiat)
                combined = self._combine_orderbook(combined, book, service)
                self.got_orderbook_services.append(service)
            except NotImplementedError:
                pass
            except Exception as exc:
                print("%s orderbook failed: %s: %s" % (service.name, exc.__class__, str(exc)))

        if trim_fiat:
            combined['asks'] = self._trim(combined['asks'], trim_fiat, side='fiat')

        if trim_crypto:
            combined['bids'] = self._trim(combined['bids'], trim_crypto, side='crypto')

        return combined

    def _combine_orderbook(self, combined_book, new_book, new_book_service):
        for side in ['bids', 'asks']:
            for order in new_book[side]:
                with_name = (order[0], order[1], new_book_service)
                combined_book[side].append(with_name)

        combined_book['bids'] = sorted(combined_book['bids'], key=lambda x: x[0], reverse=True)
        combined_book['asks'] = sorted(combined_book['asks'], key=lambda x: x[0])

        return combined_book

    def _trim(self, book, balances, side):
        """
        >>> m = MultiOrderbook()
        >>> book = [
            [7800, 1.1, GDAX()],
            [7805, 3.2, Poloniex()],
            [7810, 0.3, GDAX()],
            [7900, 7.0, GDAX()]
        ]
        >>> m._trim(book, {GDAX(): 1.2, Poloniex(): 1.0}, 'crypto')
        [[7800, 1.1, <Service: GDAX (0 in cache)>],
         [7805, 1.0, <Service: Poloniex (0 in cache)>],
         [7810, 0.1, <Service: GDAX (0 in cache)>]]
        """
        new_book = []
        for service, balance in balances.items():
            accumulation = 0
            for order in book:
                if order[2].name == service.name:
                    if side == 'crypto':
                        size = order[1]
                    else:
                        size = order[1] * order[0] # convert to fiat

                    if size + accumulation <= balance:
                        new_book.append(order)
                        accumulation += size
                    else:
                        remaining = balance - accumulation
                        new_book.append([order[0], float("%.8f" % remaining), order[2]])
                        break

        return sorted(new_book, key=lambda x: x[0], reverse=side == 'fiat')

    def _services_from_balances(self, bal1, bal2):
        return (
            set(s for s,b in bal1.items() if b > 0) |
            set(s for s,b in bal2.items() if b > 0)
        )
