# coding=utf-8
import yaml
import logging
from asyncio import wait, ensure_future, get_event_loop
from mephisto import exchanges

msg_fmt = '''
For {currency}, the ask at {exchange1} is lower than the bid at {exchange2} for the {market} market.
We would be buying {volume} {currency} for {cost:.8f} and selling for {sale:.8f}.
In total we would be buying {cost_vol:.8f} BTC worth and selling it for {sale_vol:.8f}.

This would yield {profit:.8f} {instrument} (${profitdollars:.2f}) after fees.
'''
arbitrate_fmt = '!!!!! {0}: Would BUY {currency} on {exchange1} for {cost:.8f} and SELL on {exchange2} for {sale:.8f}'
no_go_fmt = '{0}: NO OPPORTUNITY (Best case is {1:.8f}, of which {2:.8f} are fees)'

markets = [
    'BTC-ETH',
    'BTC-LTC',
    'BTC-XRP',
    'BTC-XZC',
    'BTC-OMG',
]

volume = 1
fee = 2.5 / 100
ignore_small_profits = False

with open('etc/auth.yaml') as f:
    y = yaml.load(f)

log = logging.getLogger(__name__)
log.setLevel(logging.INFO)
handler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s')
handler.setFormatter(formatter)
log.addHandler(handler)

log.info('Creating event loop')
jobs = get_event_loop()
job_queue = list()

log.info('Start: Adding exchanges')
clients = dict()
for exchange_name, auth_keys in y.items():
    clients[exchange_name] = exchanges[exchange_name](
        private_key=auth_keys['private-key'],
        public_key=auth_keys['public-key']
    )
log.info('Finish: Adding exchanges')


log.info('Start: Retrieving BTC price')
current_btc_aud = int(clients['btcmarkets'].best_bid('AUD-BTC'))
log.info('Finish: Retrieving BTC price (${0})'.format(current_btc_aud))


min_max = {
    'ask': min,
    'bid': max,
}
lookup = {market: {} for market in markets}
data = {market: {} for market in markets}


def poll_exchange(exchange, market, mode, data_target, lookup_target):
    _data, _lookup = data_target[market], lookup_target[market]
    try:
        if mode == 'bid':
            log.debug('Polling {0} for {1} on market {2}'.format(exchange, market, mode))
            ret = exchange.best_bid(market)
        elif mode == 'ask':
            log.debug('Polling {0} for {1} on market {2}'.format(exchange, market, mode))
            ret = exchange.best_ask(market)
        else:
            raise ValueError('Mode needs to be bid or ask')
        if isinstance(ret, dict):
            raise KeyError
    except (KeyError, TypeError):
        pass
    else:
        _data.update({
            mode: min_max[mode](_data.get(mode, ret), ret)
        })
        _lookup.update({
            ret: str(exchange)
        })


async def poll(loop, *args):
    await loop.run_in_executor(None, poll_exchange, *args)

log.info('Start: main')
while True:
    log.info('Start: Adding polling jobs')
    for _market in markets:
        for name, client in clients.items():
            bid_task = ensure_future(poll(jobs, client, _market, 'bid', data, lookup))
            ask_task = ensure_future(poll(jobs, client, _market, 'ask', data, lookup))
            job_queue.append(bid_task)
            job_queue.append(ask_task)
    log.info('Finish: Adding polling jobs')

    log.info('Start: Execute {0} jobs for polling'.format(len(job_queue)))
    jobs.run_until_complete(wait(job_queue))
    log.info('Finish: Execute {0} jobs for polling'.format(len(job_queue)))
    job_queue.clear()

    # pprint(data)
    # pprint(lookup)

    for _market in markets:
        if len(lookup[_market]) / 2 == 1:
            # Skip results where we only have prices from 1 exchange
            continue
        try:
            max_bid = data[_market]['bid']
            min_ask = data[_market]['ask']
            if max_bid == 'error' or min_ask == 'error':
                raise ValueError
        except (ValueError, TypeError):
            continue
        else:
            fees = (max_bid * fee) + (min_ask * fee)
            profit = (max_bid - min_ask - fees) * volume
            profit_aud = profit * current_btc_aud

        if profit_aud > 0:
            if profit_aud < 50 and ignore_small_profits:
                log.info('Arbitrage opportunity too small')
            else:
                instrument, currency = _market.split('-')
                print(arbitrate_fmt.format(
                    currency=currency,
                    instrument=instrument,
                    exchange1=lookup[_market][min_ask],
                    exchange2=lookup[_market][max_bid],
                    cost=min_ask,
                    sale=max_bid,
                ))
                # log.info(msg_fmt.format(
                #     exchange1=lookup[_market][min_ask],
                #     exchange2=lookup[_market][max_bid],
                #     market=_market,
                #     cost=min_ask,
                #     cost_vol=min_ask * volume,
                #     sale=max_bid,
                #     sale_vol=max_bid * volume,
                #     currency=currency,
                #     instrument=instrument,
                #     volume=volume,
                #     profit=profit,
                #     profitdollars=profit_aud,
                # ))
                # buy from min_ask
                # sell to max_bid
        else:
            log.info(no_go_fmt.format(_market, profit, fees))
