from __future__ import print_function
import sys
from binascii import hexlify
from tabulate import tabulate
import hashlib

from base58 import b58decode_check

from .core import (
    AutoFallbackFetcher, enforce_service_mode, get_optimal_services, get_magic_bytes,
    RevertToPrivateMode, CurrencyNotSupported, NoService, NoServicesDefined
)
from .historical_price import Quandl
from .crypto_data import crypto_data
from bitcoin import sha256, pubtoaddr, privtopub, encode_privkey, encode_pubkey, privkey_to_address

is_py2 = False
if sys.version_info <= (3,0):
    is_py2 = True

class CompositeResponse(object):
    def __init__(self, service1, service2):
        self.service1 = service1
        self.service2 = service2

    def json(self):
        return {
            self.service1.name: self.service1.last_raw_response.json(),
            self.service2.name: self.service2.last_raw_response.json(),
        }

class CompositeService(object):
    """
    This object mimicks the Service class and is used when the price fetcher has
    to fetch two different price sources. This object is only used when invoking
    `report_services`.
    """
    def __init__(self, services1, services2, via):
        service1 = services1[0]
        service2 = services2[0]

        self.name = "%s -> %s (via %s)" % (
            service1.name, service2.name, via.upper()
        )
        self.last_url = "%s, %s" % (service1.last_url, service2.last_url)
        self.last_raw_response = CompositeResponse(service1, service2)
        self.service_id = "%d+%d" % (service1.service_id, service2.service_id)

    def __repr__(self):
        return "<Composite Service: %s>" % self.name


def _try_price_fetch(services, args, modes):
    try:
        return enforce_service_mode(
            services, CurrentPrice, args, modes=modes
        )
    except NoService as exc:
        return exc

def get_current_price(crypto, fiat, services=None, convert_to=None, helper_prices=None, **modes):
    """
    High level function for getting current exchange rate for a cryptocurrency.
    If the fiat value is not explicitly defined, it will try the wildcard service.
    if that does not work, it tries converting to an intermediate cryptocurrency
    if available.
    """
    fiat = fiat.lower()
    args = {'crypto': crypto, 'fiat': fiat, 'convert_to': convert_to}

    if not services:
        services = get_optimal_services(crypto, 'current_price')

    if fiat in services:
        # first, try service with explicit fiat support
        try_services = services[fiat]
        result = _try_price_fetch(try_services, args, modes)
        if not isinstance(result, Exception):
            return result

    if '*' in services:
        # then try wildcard service
        try_services = services['*']
        result = _try_price_fetch(try_services, args, modes)
        if not isinstance(result, Exception):
            return result

    def _do_composite_price_fetch(crypto, convert_crypto, fiat, helpers, modes):
        before = modes.get('report_services', False)
        modes['report_services'] = True
        services1, converted_price = get_current_price(crypto, convert_crypto, **modes)
        if not helpers or convert_crypto not in helpers[fiat]:
            services2, fiat_price = get_current_price(convert_crypto, fiat, **modes)
        else:
            services2, fiat_price = helpers[fiat][convert_crypto]

        modes['report_services'] = before

        if modes.get('report_services', False):
            serv = CompositeService(services1, services2, convert_crypto)
            return [serv], converted_price * fiat_price
        else:
            return converted_price * fiat_price

    for composite_attempt in ['btc', 'ltc', 'doge', 'uno']:
        if composite_attempt in services and services[composite_attempt]:
            result = _do_composite_price_fetch(
                crypto, composite_attempt, fiat, helper_prices, modes
            )
            if not isinstance(result, Exception):
                return result

    raise result

def get_fiat_exchange_rate(from_fiat, to_fiat):
    from moneywagon.services import FreeCurrencyConverter
    c = FreeCurrencyConverter()
    return c.get_fiat_exchange_rate(from_fiat, to_fiat)

def get_address_balance(crypto, address=None, addresses=None, services=None, **modes):
    if not services:
        services = get_optimal_services(crypto, 'address_balance')

    args = {'crypto': crypto}

    if address:
        args['address'] = address
    elif addresses:
        args['addresses'] = addresses
    else:
        raise Exception("Either address or addresses but not both")

    results = enforce_service_mode(
        services, AddressBalance, args, modes=modes
    )

    if modes.get('private') and addresses:
        results['total_balance'] = sum(results.values())

    if modes.get('private') and modes.get('report_services', False):
        # private mode does not return services (its not practical),
        # an empty list is returned in its place to simplify the API.
        return [], results

    return results

def get_historical_transactions(crypto, address=None, addresses=None, services=None, **modes):
    if not services:
        services = get_optimal_services(crypto, 'historical_transactions')

    kwargs = {'crypto': crypto}
    if addresses:
        kwargs['addresses'] = addresses
    if address:
        kwargs['address'] = address

    try:
        txs = enforce_service_mode(
            services, HistoricalTransactions, kwargs, modes=modes
        )
    except RevertToPrivateMode:
        # no services implement get_historical_transactions_multi...
        modes['private'] = 1
        if modes.get('verbose'):