
import time
import json
import base64
import msgpack
from schema import Schema, And, Optional
from datetime import datetime
from algosdk import mnemonic
from algosdk.account import address_from_private_key
from algosdk.error import *
from algosdk.future.transaction import PaymentTxn
from inequality_indexes import *
from algo_query import *


def wait_for_confirmation(algod_client, transaction_id, timeout):
    """Wait until the transaction is confirmed or rejected, or until 'timeout'
    number of rounds have passed.

    Args:
        algod_client (AlgodClient): Algod Client
        transaction_id (str): the transaction to wait for
        timeout (int): maximum number of rounds to wait

    Returns:
        (dict): pending transaction information, or throws an error if the
        transaction is not confirmed or rejected in the next timeout rounds
    """
    start_round = algod_client.status()["last-round"] + 1
    current_round = start_round

    while current_round < start_round + timeout:
        algod_client.status_after_block(current_round)
        try:
            pending_txn = algod_client.pending_transaction_info(transaction_id)
        except Exception:
            return
        if pending_txn.get("confirmed-round", 0) > 0:
            return pending_txn
        elif pending_txn["pool-error"]:
            raise Exception(
                'pool error: {}'.format(pending_txn["pool-error"]))
        current_round += 1
    raise Exception(
        'pending tx not found in timeout rounds, timeout value = : {}'.format(
            timeout))


def post_ppos_dex_data(algod_client, indexer_client, passphrase,
                       algo_threshold):

    private_key = mnemonic.to_private_key(passphrase)

    account = {'pk': address_from_private_key(private_key),
               'sk': private_key}

    CONNECTION_ATTEMPT_DELAY_SEC = 3
    MAX_CONNECTION_ATTEMPTS = 10
    MICROALGO_TO_ALGO = 1 / 10 ** 6
    MICROALGO_TOTAL_SUPPLY = 10 ** 16

    attempts = 1
    params = None
    ledger = None
    while attempts <= MAX_CONNECTION_ATTEMPTS:
        try:
            params = algod_client.suggested_params()
            ledger = algod_client.ledger_supply()
            break
        except AlgodHTTPError:
            print(f"Algod Client connection attempt "
                  f"{attempts}/{MAX_CONNECTION_ATTEMPTS}")
            print("Trying to contact Algod Client again...")
            time.sleep(CONNECTION_ATTEMPT_DELAY_SEC)
        finally:
            attempts += 1
    if attempts > MAX_CONNECTION_ATTEMPTS:
        quit("Unable to connect to Algod Client.")

    attempts = 1
    algo_owners = None
    while attempts <= MAX_CONNECTION_ATTEMPTS:
        try:
            algo_owners = get_algo_owners(indexer_client, algo_threshold)
            break
        except IndexerHTTPError:
            print(f"Indexer Client connection attempt "
                  f"{attempts}/{MAX_CONNECTION_ATTEMPTS}")
            print("Trying to contact Indexer Client again...")
            time.sleep(CONNECTION_ATTEMPT_DELAY_SEC)
        finally:
            attempts += 1
    if attempts > MAX_CONNECTION_ATTEMPTS:
        quit("Unable to connect to Indexer Client.")

    stakes = [account['amount'] * MICROALGO_TO_ALGO for
              account in algo_owners]
    algo_hhi = herfindahl_hirschman_index(stakes)
    online_stakes = [account['amount'] * MICROALGO_TO_ALGO
                     for account in algo_owners
                     if account['status'] == 'Online']
    algo_dynamics = ledger['total-money'] / MICROALGO_TOTAL_SUPPLY
    ppos_online_stake = ledger['online-money'] / ledger['total-money']
    ppos_online_accounts = len(online_stakes) / len(algo_owners)
    ppos_gini = gini_index(online_stakes)
    ppos_theil_l = theil_l_index(online_stakes)
    ppos_theil_t = theil_t_index(online_stakes)
    ppos_hhi = herfindahl_hirschman_index(online_stakes)
    ppos_dex = (algo_dynamics
                * ppos_online_stake
                * ppos_online_accounts
                * (1 - ppos_gini))

    note = {'algo_threshold': algo_threshold,
            'accounts': len(algo_owners),
            'algo_hhi': algo_hhi,
            'algo_dynamics': algo_dynamics,
            'ppos_online_stake': ppos_online_stake,
            'ppos_online_accounts': ppos_online_accounts,
            'ppos_gini': ppos_gini,
            'ppos_theil_l': ppos_theil_l,
            'ppos_theil_t': ppos_theil_t,
            'ppos_hhi': ppos_hhi,
            'ppos_dex': ppos_dex,
            'timestamp': str(datetime.now())}

    bytes_note = msgpack.packb(note)

    unsigned_txn = PaymentTxn(sender=account['pk'],
                              sp=params,
                              receiver=account['pk'],
                              amt=0,
                              note=bytes_note)

    signed_txn = unsigned_txn.sign(account['sk'])
    txid = algod_client.send_transaction(signed_txn)
    print("Publishing Algorand PPoS Dex data in txID: {}".format(txid))

    try:
        confirmed_txn = wait_for_confirmation(algod_client, txid, 4)
    except Exception as err:
        print(err)
        return

    print("txID: {}".format(txid), " confirmed in round: {}\n".format(
        confirmed_txn.get("confirmed-round", 0)))
    print("Transaction information:\n{}".format(
        json.dumps(confirmed_txn, indent=4)))


def get_ppos_dex_data(indexer_client, ppos_dex_address, algo_threshold,
                      start_block=11476070, end_block=None):

    CONNECTION_ATTEMPT_DELAY_SEC = 3
    MAX_CONNECTION_ATTEMPTS = 10

    attempts = 1
    ppos_dex_txns_note = None
    while attempts <= MAX_CONNECTION_ATTEMPTS:
        try:
            ppos_dex_txns_note = get_address_txns_note(
                indexer_client, ppos_dex_address, start_block, end_block)
            break
        except IndexerHTTPError:
            print(f"Indexer Client connection attempt "
                  f"{attempts}/{MAX_CONNECTION_ATTEMPTS}")
            print("Trying to contact Indexer Client again...")
            time.sleep(CONNECTION_ATTEMPT_DELAY_SEC)
        finally:
            attempts += 1
    if attempts > MAX_CONNECTION_ATTEMPTS:
        quit("Unable to connect to Indexer Client.")

    # TODO: make 'algo_hhi' and 'ppos_hhi' mandatory fileds in the schema
    schema = Schema({
        'algo_threshold': int,
        'accounts': And(int, lambda n: 0 <= n),
        Optional('algo_hhi'): And(float, lambda n: 0 <= n <= 1),
        'algo_dynamics': And(float, lambda n: 0 <= n),
        'ppos_online_stake': And(float, lambda n: 0 <= n <= 1),
        'ppos_online_accounts': And(float, lambda n: 0 <= n <= 1),
        'ppos_gini': And(float, lambda n: 0 <= n <= 1),
        'ppos_theil_l': And(float, lambda n: 0 <= n),
        'ppos_theil_t': And(float, lambda n: 0 <= n),
        Optional('ppos_hhi'): And(float, lambda n: 0 <= n <= 1),
        'ppos_dex': And(float, lambda n: 0 <= n <= 1),
        'timestamp': str
    })

    ppos_dex_data = []
    for txn_note in ppos_dex_txns_note:
        try:
            data = schema.validate(
                msgpack.unpackb(base64.b64decode(txn_note))
            )
            if data['algo_threshold'] == algo_threshold:
                ppos_dex_data += [data]
        except:
            pass

    if not ppos_dex_data:
        quit(f"Impossible to find valid PPos Dex data published by "
             f"{ppos_dex_address} starting from block {start_block}.")

    return ppos_dex_data

[((52, 18, 52, 53), 'algosdk.mnemonic.to_private_key', 'mnemonic.to_private_key', ({(52, 42, 52, 52): 'passphrase'}, {}), '(passphrase)', False, 'from algosdk import mnemonic\n'), ((127, 17, 127, 36), 'msgpack.packb', 'msgpack.packb', ({(127, 31, 127, 35): 'note'}, {}), '(note)', False, 'import msgpack\n'), ((129, 19, 133, 46), 'algosdk.future.transaction.PaymentTxn', 'PaymentTxn', (), '', False, 'from algosdk.future.transaction import PaymentTxn\n'), ((54, 21, 54, 58), 'algosdk.account.address_from_private_key', 'address_from_private_key', ({(54, 46, 54, 57): 'private_key'}, {}), '(private_key)', False, 'from algosdk.account import address_from_private_key\n'), ((125, 29, 125, 43), 'datetime.datetime.now', 'datetime.now', ({}, {}), '()', False, 'from datetime import datetime\n'), ((148, 8, 148, 43), 'json.dumps', 'json.dumps', (), '', False, 'import json\n'), ((178, 8, 178, 28), 'schema.Optional', 'Optional', ({(178, 17, 178, 27): '"""algo_hhi"""'}, {}), "('algo_hhi')", False, 'from schema import Schema, And, Optional\n'), ((185, 8, 185, 28), 'schema.Optional', 'Optional', ({(185, 17, 185, 27): '"""ppos_hhi"""'}, {}), "('ppos_hhi')", False, 'from schema import Schema, And, Optional\n'), ((177, 20, 177, 46), 'schema.And', 'And', ({(177, 24, 177, 27): 'int', (177, 29, 177, 45): 'lambda n: 0 <= n'}, {}), '(int, lambda n: 0 <= n)', False, 'from schema import Schema, And, Optional\n'), ((178, 30, 178, 63), 'schema.And', 'And', ({(178, 34, 178, 39): 'float', (178, 41, 178, 62): 'lambda n: 0 <= n <= 1'}, {}), '(float, lambda n: 0 <= n <= 1)', False, 'from schema import Schema, And, Optional\n'), ((179, 25, 179, 53), 'schema.And', 'And', ({(179, 29, 179, 34): 'float', (179, 36, 179, 52): 'lambda n: 0 <= n'}, {}), '(float, lambda n: 0 <= n)', False, 'from schema import Schema, And, Optional\n'), ((180, 29, 180, 62), 'schema.And', 'And', ({(180, 33, 180, 38): 'float', (180, 40, 180, 61): 'lambda n: 0 <= n <= 1'}, {}), '(float, lambda n: 0 <= n <= 1)', False, 'from schema import Schema, And, Optional\n'), ((181, 32, 181, 65), 'schema.And', 'And', ({(181, 36, 181, 41): 'float', (181, 43, 181, 64): 'lambda n: 0 <= n <= 1'}, {}), '(float, lambda n: 0 <= n <= 1)', False, 'from schema import Schema, And, Optional\n'), ((182, 21, 182, 54), 'schema.And', 'And', ({(182, 25, 182, 30): 'float', (182, 32, 182, 53): 'lambda n: 0 <= n <= 1'}, {}), '(float, lambda n: 0 <= n <= 1)', False, 'from schema import Schema, And, Optional\n'), ((183, 24, 183, 52), 'schema.And', 'And', ({(183, 28, 183, 33): 'float', (183, 35, 183, 51): 'lambda n: 0 <= n'}, {}), '(float, lambda n: 0 <= n)', False, 'from schema import Schema, And, Optional\n'), ((184, 24, 184, 52), 'schema.And', 'And', ({(184, 28, 184, 33): 'float', (184, 35, 184, 51): 'lambda n: 0 <= n'}, {}), '(float, lambda n: 0 <= n)', False, 'from schema import Schema, And, Optional\n'), ((185, 30, 185, 63), 'schema.And', 'And', ({(185, 34, 185, 39): 'float', (185, 41, 185, 62): 'lambda n: 0 <= n <= 1'}, {}), '(float, lambda n: 0 <= n <= 1)', False, 'from schema import Schema, And, Optional\n'), ((186, 20, 186, 53), 'schema.And', 'And', ({(186, 24, 186, 29): 'float', (186, 31, 186, 52): 'lambda n: 0 <= n <= 1'}, {}), '(float, lambda n: 0 <= n <= 1)', False, 'from schema import Schema, And, Optional\n'), ((74, 12, 74, 52), 'time.sleep', 'time.sleep', ({(74, 23, 74, 51): 'CONNECTION_ATTEMPT_DELAY_SEC'}, {}), '(CONNECTION_ATTEMPT_DELAY_SEC)', False, 'import time\n'), ((90, 12, 90, 52), 'time.sleep', 'time.sleep', ({(90, 23, 90, 51): 'CONNECTION_ATTEMPT_DELAY_SEC'}, {}), '(CONNECTION_ATTEMPT_DELAY_SEC)', False, 'import time\n'), ((168, 12, 168, 52), 'time.sleep', 'time.sleep', ({(168, 23, 168, 51): 'CONNECTION_ATTEMPT_DELAY_SEC'}, {}), '(CONNECTION_ATTEMPT_DELAY_SEC)', False, 'import time\n'), ((194, 32, 194, 58), 'base64.b64decode', 'base64.b64decode', ({(194, 49, 194, 57): 'txn_note'}, {}), '(txn_note)', False, 'import base64\n')]