#!/usr/bin/env python

from __future__ import print_function

import sys
import argparse
import json
import datetime

from concurrent import futures
from tabulate import tabulate

from moneywagon import (
    CurrentPrice, HistoricalPrice, AddressBalance, get_address_balance,
    get_historical_transactions, get_block, get_unspent_outputs, get_current_price,
    generate_keypair, sweep, get_explorer_url, service_table, get_single_transaction
)
from moneywagon.wallet import fetch_wallet_balances

parser = argparse.ArgumentParser() #version='1.0.2')

subparsers = parser.add_subparsers(help='commands', dest="subparser_name")

x = subparsers.add_parser('current-price', help='Get current price of a crypto/fiat pair.')
x.add_argument('crypto', action='store', help='Cryptocurrency symbol')
x.add_argument('fiat', action='store', help='Fiat currency symbol')
x.add_argument('--verbose', action='store_true', help='Include extra output')
x.add_argument('--random-service', action='store_true', help='Use a random source')
x.add_argument('--timeout', action='store', help='Time until giving up when making external calls. In seconds.')

x = subparsers.add_parser('generate-keypair', help='Generate Private/Public key pair')
x.add_argument('crypto', action='store', help='Cryptocurrency symbol')
x.add_argument('seed', action='store', help='Random seed string.'),
x.add_argument('--password', action='store', help='Encrypt private key with Bip38 password.')
x.add_argument('--verbose', action='store_true', help='Include extra output')
x.add_argument('--timeout', action='store', help='Time until giving up when making external calls. In seconds.')

x = subparsers.add_parser('historical-price', help='Get price of a crypto/fiat pair at a point in time.')
x.add_argument('crypto', action='store', help='Cryptocurrency symbol')
x.add_argument('fiat', action='store', help='Fiat currency symbol')
x.add_argument('at_time', action='store', help='Time when to get the price. e.g. 2014-04-03')
x.add_argument('--verbose', action='store_true', help='Include extra output')
x.add_argument('--timeout', action='store', help='Time until giving up when making external calls. In seconds.')

x = subparsers.add_parser('address-balance', help='Get total amount of coin in wallet.')
x.add_argument('crypto', action='store', help='Cryptocurrency symbol')
x.add_argument('--address', action='store', help='Wallet address')
x.add_argument('--addresses', action='store', help='Comma seperated list of wallet addresses')
x.add_argument('--paranoid', action='store', help='How many services to use when cross-checking')
x.add_argument('--verbose', action='store_true', help='Include extra output')
#x.add_argument('--fast', action='store_true', help='Return fast as possible.')
x.add_argument('--random-service', action='store_true', help='Use a random source')
x.add_argument('--timeout', action='store', help='Time until giving up when making external calls. In seconds.')

x = subparsers.add_parser('single-transaction', help='Get Information about a single transaction.')
x.add_argument('crypto', action='store', help='Cryptocurrency symbol')
x.add_argument('--txid', action='store', help='Transaction ID (txid)')
x.add_argument('--txids', action='store', help='Comma seperated list of Transaction ID (txid)')
x.add_argument('--paranoid', action='store', help='How many services to use when cross-checking')
x.add_argument('--verbose', action='store_true', help='Include extra output')
#x.add_argument('--fast', action='store_true', help='Return fast as possible.')
x.add_argument('--random-service', action='store_true', help='Use a random source')
x.add_argument('--timeout', action='store', help='Time until giving up when making external calls. In seconds.')

x = subparsers.add_parser('get-block', help='Get block by either height, hash or by latest.')
x.add_argument('crypto', action='store', help='Cryptocurrency symbol')
x.add_argument('--block_number', action='store', help='Get block by block number')
x.add_argument('--block_hash', action='store', help='Get block by block hash')
x.add_argument('--latest', action='store_true', help='Get the latest block.')
x.add_argument('--paranoid', action='store', help='How many services to use when cross-checking')
x.add_argument('--verbose', action='store_true', help='Include extra output')
x.add_argument('--random-service', action='store_true', help='Use a random source')
x.add_argument('--timeout', action='store', help='Time until giving up when making external calls. In seconds.')

x = subparsers.add_parser('historical-transactions', help='Get list of all transactions for this address.')
x.add_argument('crypto', action='store', help='Cryptocurrency symbol')
x.add_argument('--address', action='store', help='Wallet address')
x.add_argument('--addresses', action='store', help='Comma seperated list of wallet addresses')
x.add_argument('--paranoid', action='store', help='How many services to use when cross-checking')
x.add_argument('--verbose', action='store_true', help='Include extra output')
x.add_argument('--random-service', action='store_true', help='Use a random source')
x.add_argument('--timeout', action='store', help='Time until giving up when making external calls. In seconds.')

x = subparsers.add_parser('sweep', help='Sweep funds from a private key to another address')
x.add_argument('crypto', action='store', help='Cryptocurrency symbol')
x.add_argument('private_key', action='store', help='Private key to draw funds from')
x.add_argument('to_address', action='store', help='Address to send funds to.')
x.add_argument('--password', action='store', help='Decrypt private key with Bip38 password.')
x.add_argument('--fee', action='store', help='Fee to use for tx (in satoshi). Defaults to $0.02', default=None)
x.add_argument('--paranoid', action='store', help='How many services to use when cross-checking')
x.add_argument('--verbose', action='store_true', help='Include extra output')
x.add_argument('--random-service', action='store_true', help='Use a random source')

x = subparsers.add_parser('unspent-outputs', help='Get list of unspent outputs for this address.')
x.add_argument('crypto', action='store', help='Cryptocurrency symbol')
x.add_argument('--address', action='store', help='Wallet address')
x.add_argument('--addresses', action='store', help='Comma seperated list of wallet addresses')
x.add_argument('--paranoid', action='store', help='How many services to use when cross-checking')
x.add_argument('--verbose', action='store_true', help='Include extra output')
x.add_argument('--random-service', action='store_true', help='Use a random source')
x.add_argument('--timeout', action='store', help='Time until giving up when making external calls. In seconds.')

x = subparsers.add_parser('explorer-urls', help='Get URLS for web-based block explorers.')
x.add_argument('crypto', action='store', help='Cryptocurrency symbol')
x.add_argument('--address', action='store', help='Wallet address')
x.add_argument('--blocknum', action='store', help='Block Number (height)')
x.add_argument('--blockhash', action='store', help='Block Height')

x = subparsers.add_parser('service-table', help='Get list of all currently implemented services.')
x.add_argument('--format', action='store', help='Table format output, e.g. grid, simple, rst, html')

x = subparsers.add_parser('wallet-balance', help='Get current value in fiat of a set of crypo addresses.')
x.add_argument('wallet', type=argparse.FileType('r'), default=sys.stdin, help='Wallet file')
x.add_argument('fiat', action='store', help='Fiat currency')
x.add_argument('--paranoid', action='store', help='How many services to use when cross-checking')
x.add_argument('--verbose', action='store_true', help='Include extra output')
x.add_argument('--euro-format', action='store_true', help='Use european comma style, e.g 2.132.324,87')
x.add_argument('--async', action='store_true', help='Fetch prices and balances asynchronously')
x.add_argument('--random-service', action='store_true', help='Use random sources')
x.add_argument('--format', action='store', help='Table format output, e.g. grid, simple, rst, html')
x.add_argument("--collapse", action='store_true', help="Collapse same cryptos into a single line.")
x.add_argument('--timeout', action='store', help='Time until giving up when making external calls. In seconds.')

argz = parser.parse_args()

def prepare_json(high_level_func, *args, **kwargs):
    """
    The low-level API returns the service list along with it's results when paranid
    mode is invoked. For the command line, we don't want to show them.
    """
    def datetime_to_iso(obj):
        """
        Python's default json encoder will blow up when it encounters datetime objects.
        So this work around is needed in order to just handle making datetime
        objects into iso8601 string format.
        """
        if isinstance(obj, datetime.datetime):
            serial = obj.isoformat()
            return serial
        raise TypeError("Type not serializable")

    result = high_level_func(*args, **kwargs)
    return json.dumps(result, default=datetime_to_iso)

modes = {
    'random': argz.random_service if hasattr(argz, "random_service") else False,
    'paranoid': int(argz.paranoid or 1) if hasattr(argz, 'paranoid') else 1,
    'verbose': argz.verbose if hasattr(argz, "verbose") else False,
    'fast': 1 if hasattr(argz, "fast") else False,
    'timeout': float(argz.timeout or 0) if hasattr(argz, "timeout") else None
}

if argz.subparser_name == 'current-price':
    price = get_current_price(argz.crypto, argz.fiat, verbose=argz.verbose)
    print(price)

elif argz.subparser_name == 'generate-keypair':
    if argz.seed == '-':
        seed =  "".join(sys.stdin)
    else:
        seed = argz.seed

    print(json.dumps(generate_keypair(argz.crypto, seed, password=argz.password)))

elif argz.subparser_name == 'historical-price':
    price, source, date = HistoricalPrice(verbose=argz.verbose).action(argz.crypto, argz.fiat, argz.at_time)
    print(price, source, date)

elif argz.subparser_name == 'address-balance':
    if argz.address:
        modes['address'] = argz.address
    elif argz.addresses:
        modes['addresses'] = argz.addresses
    else:
        raise Exception('Either address or addresses argument required')

    print(prepare_json(
        get_address_balance, argz.crypto, **modes
    ))

elif argz.subparser_name == 'single-transaction':
    if argz.txid:
        modes['txid'] = argz.txid
    elif argz.txids:
        modes['txids'] = argz.txids
    else:
        raise Exception('Either txid or txids argument required')

    print(prepare_json(
        get_single_transaction, argz.crypto, **modes
    ))

elif argz.subparser_name == 'get-block':
    print(prepare_json(
        get_block, argz.crypto, block_number=argz.block_number or '',
        block_hash=argz.block_hash or '', latest=argz.latest or False, modes=modes
    ))

elif argz.subparser_name == 'historical-transactions':
    if argz.address:
        modes['address'] = argz.address
    elif argz.addresses:
        modes['addresses'] = argz.addresses
    else:
        raise Exception('Either address or addresses argument required')
    print(prepare_json(
        get_historical_transactions, argz.crypto, **modes
    ))

elif argz.subparser_name == "unspent-outputs":
    if argz.address:
        modes['address'] = argz.address
    elif argz.addresses:
        modes['addresses'] = argz.addresses
    else:
        raise Exception('Either address or addresses argument required')

    print(prepare_json(
        get_unspent_outputs, argz.crypto, **modes
    ))

elif argz.subparser_name == 'sweep':
    print(sweep(argz.crypto, argz.private_key, argz.to_address, argz.fee, **modes))

elif argz.subparser_name == 'explorer-urls':
    if argz.address:
        print(" ".join(get_explorer_url(argz.crypto, address=argz.address)))
    if argz.blocknum:
        print(" ".join(get_explorer_url(argz.crypto, blocknum=argz.blocknum)))

elif argz.subparser_name == 'wallet-balance':
    euro = argz.euro_format
    def localized_number(num, euro=False):
        ret = "{:,.2f}".format(num)
        if euro:
            ret = ret.replace(",", "&").replace(".", ",").replace("&", ".")
        return ret

    wallets = [
        (x.split(",")[0], x.split(",")[1].strip())
        for x in argz.wallet.readlines()
        if not x.startswith("#")
    ]

    if argz.async:
        modes['async'] = True

    fiat = argz.fiat.upper()

    rows = []
    cumm_amount = 0
    errors = []
    for d in fetch_wallet_balances(wallets, fiat, **modes):
        cumm_amount += d['fiat_value']
        rows.append([
            d['crypto'].upper(), d['crypto_value'], d['fiat_value'],
            d['conversion_price'], d['price_source']
        ])
        if d['error']:
            errors.append(d['error'])

    if argz.collapse:
        # collapse similar cryptos into a single line
        new_rows = {}
        for wallet in rows:
            crypto = wallet[0]
            if crypto in new_rows:
                old_row = new_rows[crypto]
                new_rows[crypto] = [
                    crypto,
                    wallet[1] + old_row[1], # crypto balance
                    wallet[2] + old_row[2], # fiat value
                    old_row[3], # exchange
                    old_row[4] # source
                ]
            else:
                new_rows[crypto] = wallet

        rows = new_rows.values()

    rows.sort(key=lambda x: x[2], reverse=True) # order by highest fiat value first

    # combine fiat value and exchange rate into single cell
    rows = [
        [x[0], x[1], "%s %s" % (localized_number(x[2], euro), fiat), "%f %s/%s" % (x[3], fiat, x[0]), x[4]]
        for x in rows
    ]

    rows.append(['------', 'Total:', "%s %s" % (localized_number(cumm_amount, euro), fiat), '------'])

    headers = ['Crypto', 'Balance', 'Fiat Value', 'Exchange Rate', 'Price Source']
    print(tabulate(rows, headers=headers, tablefmt=argz.format))
    for error in errors:
        print(error)

elif argz.subparser_name == 'service-table':
    print(service_table(format=argz.format or 'simple'))
