import argparse
import sys
import time
from pprint import pprint
from enum import Enum, auto

import requests
from solana.keys import PrivateKey

from solana.account import Account
from solana.transaction import Transaction
from solana.system_program import SystemProgram
from solana.system_program import transfer
from solana.program import Program
from solana.transaction import TransactionError

NUM_CREDITS = 61  # The number of credits to transfer
NUM_TXNS = 100  # The number of transactions that will be sent
THREADS = 4  # The number of threads to send the transactions
HTTP_TIMEOUT = 120  # The time in seconds to wait for an HTTP invocation
CLI_TIMEOUT = 30  # The time in seconds to wait for a CLI invocation

# A public key pre-fund with 1M tokens
UNIVERSE_KEY = PrivateKey.public_key(
    bytes.fromhex(
        "0000000000000000000000000000000000000000000000000000000000000002"
    ))

# A public key pre-fund with 70K tokens
# Only used in CLI mode
CLI_FAUCET_KEY = PrivateKey.public_key(
    bytes.fromhex(
        "00000000000000000000000000000000000000000000000000000baf60ed77ea"
    ))

class Mode(Enum):
    ONLINE = auto()
    CLI = auto()

parser = argparse.ArgumentParser()
parser.add_argument("--faucet-address",
                    type=str,
                    help="URL for the faucet",
                    default="https://faucet.testnet.solana.com")
parser.add_argument("--faucet-delay",
                    type=int,
                    help="Amount of seconds to wait between faucet requests",
                    default=10)
parser.add_argument("--mode",
                    type=str,
                    help="The double-spend mode; either online or cli",
                    default="online",
                    choices=["online", "cli"])
parser.add_argument("--send-delay",
                    type=int,
                    help="The delay between the double spends",
                    default=0)
parser.add_argument("--send-threads",
                    type=int,
                    help="The number of threads to send transactions",
                    default=THREADS)
parser.add_argument("--cli-host",
                    type=str,
                    help="Host of an active CLI",
                    default="localhost")
parser.add_argument("--send-txns",
                    type=int,
                    help="The number of transactions that will be sent",
                    default=NUM_TXNS)
parser.add_argument("--credit-account",
                    type=str,
                    required=True,
                    help="The public key of the account to receive money")
parser.add_argument("--spend-account",
                    type=str,
                    required=True,
                    help="The public key of the account to send money from")
parser.add_argument("--spend-privkey",
                    type=str,
                    required=True,
                    help="The secret key of the account to send money from")

def get_amount(mode):
    if mode == Mode.ONLINE:
        return NUM_CREDITS
    if mode == Mode.CLI:
        return 2000

def get_faucet_account(mode):
    if mode == Mode.ONLINE:
        return UNIVERSE_KEY
    if mode == Mode.CLI:
        return CLI_FAUCET_KEY

def send_via_http(url, pubkey, private_key):
    params = {'id': 0, 'amount': NUM_CREDITS}
    response = requests.post(f"{url}/{pubkey}", params=params)
    if response.status_code != 200:
        print("FAILED TO GET TOKENS VIA HTTP")
        sys.exit(-1)

    pprint(response.text)
    return response.json()

def send_via_cli(host, address, source, source_key):
    import subprocess
    from subprocess import check_output

    cmd = f"solana config set --url http://{host}:8899"
    print(cmd)
    check_output(cmd.split())

    cmd = f"solana transfer --private-key {source_key} {address} {NUM_CREDITS} --no-wait --commit"
    print(cmd)
    check_output(cmd.split())

    cmd = f"solana balance {source} -J"
    print(cmd)
    res = check_output(cmd.split()).decode("utf-8")
    return res

def make_source_account(mode, spend_account, spend_privkey):
    if mode == Mode.ONLINE:
        return spend_account
    if mode == Mode.CLI:
        return UNIVERSE_KEY

def make_txn(credit_account, spend_account, spend_privkey, mode):

    program = transfer.program_id
    source_account = make_source_account(mode, spend_account, spend_privkey)
    amount = get_amount(mode)
    fee = 0
    recent_blockhash = SystemProgram.get_recent_blockhash()

    tx = Transaction()
    tx.add(
        PrivateKey.public_key(source_account),
        PrivateKey.public_key(credit_account),
        amount
    )

    tx.sign(spend_privkey)

    return tx

def send_transactions(rxns, mode, send_threads, host, spend_account,
                      spend_privkey, faucet_address, faucet_delay):
    import threading

    if mode == Mode.ONLINE:
        faucet_url = f"{faucet_address}/{spend_account}"
        faucet_params = {'id': 0, 'amount': NUM_CREDITS}

    if mode == Mode.CLI:
        subprocess.run(["solana", "config", "set",
                        f"--url", f"http://{host}:8899"])

    def send():
        name = threading.current_thread().name
        percent = 1
        for rxn in rxns:
            if not rxn:
                continue
            c = rxn.credits[0]
            credit_account = c.pubkey().decode()
            print(f"{name} sending to credit account: {credit_account}")
            print(f"{name} sending from spend account: {spend_account.decode()}")
            if mode == Mode.ONLINE:
                pprint(requests.post(faucet_url,
                                     params=faucet_params).json())
            if mode == Mode.CLI:
                send_via_cli(host, credit_account, spend_account,
                             spend_privkey)
                time.sleep(faucet_delay)

            time.sleep(faucet_delay)

            tx = make_txn(c.pubkey(), spend_account, spend_privkey, mode)
            # On simulate defer the sending for later
            if c.owners[0] == Program.CREDIT_PROGRAM_ID:
                c.owners[0] = tx.signatures[0]
                c.set_owner(0, tx.signatures[0])

                # Simulate execution
                c.decrement(credit_account, amount)
                print("SIMULATED CREDIT ACCOUNT: ", credit_account)
                pprint(c)
                print("SIMULATED SPEND ACCOUNT:", spend_account)
                pprint(c)
                print("\tDECREMENT TXN:")
                print("\t----------------------------------------------")
                pprint(tx)
                print("\t----------------------------------------------")
            else:
                print("\nFINAL DECREMENT TXN:")
                print("\t----------------------------------------------")
                pprint(tx)
                print("\t----------------------------------------------")

                if mode == Mode.ONLINE:
                    print("Attempting to send via HTTP...")
                    try:
                        start = time.time()
                        response = requests.post(
                            f"{host}/",
                            params={'tx': tx.to_string()},
                            timeout=HTTP_TIMEOUT)
                        end = time.time()
                        print(f"Response time: {end - start:.2f} seconds")
                        if response.status_code != 200:
                            print("FAILED TO SEND CREDIT")
                            sys.exit(-1)
                    except requests.exceptions.RequestException as e:
                        print(e)
                        print("FAILED TO SEND CREDIT")
                        sys.exit(-1)
                    pprint(response.json())
                if mode == Mode.CLI:
                    print("Sending via CLI")
                    send_via_cli(host, credit_account, spend_account,
                                 spend_privkey)

            percent += 1

    senders = [threading.Thread(target=send, name="sender%d" % i)
               for i in range(1, send_threads + 1)]
    for sender in senders:
        sender.start()

    for sender in senders:
        sender.join()

def main():
    args = parser.parse_args()
    spend_privkey = PrivateKey.from_hex(args.spend_privkey)
    spend_account = PrivateKey.public_key(args.spend_account)

    rxns = None
    if args.mode == "online":
        mode = Mode.ONLINE
        response = send_via_http(args.faucet_address, spend_account,
                                 spend_privkey)
        rxns = SystemProgram.get_recent_transactions(response['result'])
    elif args.mode == "cli":
        mode = Mode.CLI
        send_via_cli(args.cli_host, args.spend_account,
                     args.credit_account, args.spend_privkey)
        time.sleep(args.faucet_delay)
    else:
        print(f"Unknown mode: {args.mode}")
        sys.exit(-1)

    print(f"Double-spending: txns({args.send_txns}) threads({args.send_threads}) "
          f"delay(microseconds: {args.send_delay})")
    send_transactions(rxns, mode, args.send_threads, args.cli_host,
                      spend_account, spend_privkey, args.faucet_address,
                      args.faucet_delay)

if __name__ == '__main__':
    main()