#!/usr/bin/env python3
import argparse
import datetime
import hashlib
import io
import sys

from logbook import Logger, StreamHandler
import prettytable

import client

log = Logger('electrum')


def _script_hash(script):
    return hashlib.sha256(script).digest()[::-1].hex()


def show_rows(rows, field_names):
    t = prettytable.PrettyTable()
    t.field_names = field_names
    t.add_rows(rows)
    for f in t.field_names:
        if "mBTC" in f:
            t.align[f] = "r"
    print(t)


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--host', default='localhost')
    parser.add_argument('--network', default='mainnet')
    parser.add_argument('address', nargs='+')
    parser.add_argument('--only-subscribe', action='store_true', default=False)
    parser.add_argument('--no-merkle-proofs', action='store_true', default=False)
    args = parser.parse_args()

    if args.network == 'regtest':
        port = 60401
        from pycoin.symbols.xrt import network
    elif args.network == 'testnet':
        port = 60001
        from pycoin.symbols.xtn import network
    elif args.network == 'testnet4':
        port = 40001
        from pycoin.symbols.xtn import network
    elif args.network == 'signet':
        port = 60601
        from pycoin.symbols.xtn import network
    elif args.network == 'mainnet':
        port = 50001
        from pycoin.symbols.btc import network
    else:
        raise ValueError(f"unknown network: {args.network}")

    hostport = (args.host, port)
    log.info('connecting to {}:{}', *hostport)
    conn = client.Client(hostport)

    tip, = conn.call([client.request('blockchain.headers.subscribe')])

    script_hashes = [
        _script_hash(network.parse.address(addr).script())
        for addr in args.address
    ]

    conn.call(
        client.request('blockchain.scripthash.subscribe', script_hash)
        for script_hash in script_hashes
    )
    log.info('subscribed to {} scripthashes', len(script_hashes))
    if args.only_subscribe:
        return

    balances = conn.call(
        client.request('blockchain.scripthash.get_balance', script_hash)
        for script_hash in script_hashes
    )

    unspents = conn.call(
        client.request('blockchain.scripthash.listunspent', script_hash)
        for script_hash in script_hashes
    )
    for addr, balance, unspent in sorted(zip(args.address, balances, unspents), key=lambda v: v[0]):
        if unspent:
            log.debug("{}: confirmed={:,.5f} mBTC, unconfirmed={:,.5f} mBTC",
                addr, balance["confirmed"] / 1e5, balance["unconfirmed"] / 1e5)
            for u in unspent:
                log.debug("\t{}:{} = {:,.5f} mBTC {}",
                    u["tx_hash"], u["tx_pos"], u["value"] / 1e5,
                    f'@ {u["height"]}' if u["height"] else "")

    histories = conn.call(
        client.request('blockchain.scripthash.get_history', script_hash)
        for script_hash in script_hashes
    )
    txids_map = dict(
        (tx['tx_hash'], tx['height'] if tx['height'] > 0 else None) 
        for history in histories 
        for tx in history
    )
    log.info('got history of {} transactions', len(txids_map))

    txs = map(network.tx.from_hex, conn.call(
        client.request('blockchain.transaction.get', txid)
        for txid in txids_map.keys()
    ))
    txs_map = dict(zip(txids_map.keys(), txs))
    log.info('loaded {} transactions', len(txids_map))

    confirmed_txids = {txid: height for txid, height in txids_map.items() if height is not None}

    heights = set(confirmed_txids.values())
    def _parse_header(header):
        return network.block.parse_as_header(io.BytesIO(bytes.fromhex(header)))
    headers = map(_parse_header, conn.call(
        client.request('blockchain.block.header', height)
        for height in heights
    ))
    def _parse_timestamp(header):
        return datetime.datetime.utcfromtimestamp(header.timestamp).strftime('%Y-%m-%dT%H:%M:%SZ')
    timestamps = map(_parse_timestamp, headers)
    timestamps_map = dict(zip(heights, timestamps))
    log.info('loaded {} header timestamps', len(heights))

    if args.no_merkle_proofs:
        return

    proofs = conn.call(
        client.request('blockchain.transaction.get_merkle', txid, height)
        for txid, height in confirmed_txids.items()
    )
    log.info('loaded {} merkle proofs', len(proofs))  # TODO: verify proofs

    sorted_txdata = sorted(
        (proof['block_height'], proof['pos'], txid) 
        for proof, txid in zip(proofs, confirmed_txids)
    )

    utxos = {}
    balance = 0

    rows = []
    script_hashes = set(script_hashes)
    for block_height, block_pos, txid in sorted_txdata:
        tx_obj = txs_map[txid]
        for txi in tx_obj.txs_in:
            utxos.pop((str(txi.previous_hash), txi.previous_index), None)

        for index, txo in enumerate(tx_obj.txs_out):
            if _script_hash(txo.puzzle_script()) in script_hashes:
                utxos[(txid, index)] = txo
    
        diff = sum(txo.coin_value for txo in utxos.values()) - balance
        balance += diff
        confirmations = tip['height'] - block_height + 1
        rows.append([txid, timestamps_map[block_height], block_height, confirmations, f'{diff/1e5:,.5f}', f'{balance/1e5:,.5f}'])
    show_rows(rows, ["txid", "block timestamp", "height", "confirmations", "delta (mBTC)", "total (mBTC)"])

    tip_header = _parse_header(tip['hex'])
    log.info('tip={}, height={} @ {}', tip_header.id(), tip['height'], _parse_timestamp(tip_header))

    unconfirmed = {txs_map[txid] for txid, height in txids_map.items() if height is None}
    # TODO: show unconfirmed balance

if __name__ == '__main__':
    StreamHandler(sys.stderr).push_application()
    main()
