#!/usr/bin/env python3



"""
WARNING: This script makes automatic transactions.
Do not use this script unless you know what you
are doing and intend to spend ETH measuring live
policy availability.
"""

import datetime
import os
import shutil
import time
from pathlib import Path
from typing import List, Optional, Set, Tuple

import maya
from eth_typing.evm import ChecksumAddress
from nucypher_core.umbral import SecretKey
from web3 import Web3
from web3.types import Wei

from nucypher.blockchain.eth.domains import LYNX
from nucypher.blockchain.eth.signers import Signer
from nucypher.characters.lawful import Alice, Bob, Ursula
from nucypher.config.characters import AliceConfiguration
from nucypher.network.nodes import TEACHER_NODES
from nucypher.policy.payment import SubscriptionManagerPayment
from nucypher.policy.policies import Policy
from nucypher.utilities.logging import GlobalLoggerSettings

# Signer Configuration
# In order to use this script, you must configure a wallet for alice
ADDRESS_ENVVAR: str = 'NUCYPHER_GRANT_METRICS_ADDRESS'
PASSWORD_ENVVAR: str = 'NUCYPHER_GRANT_METRICS_PASSWORD'
SIGNER_ENVVAR: str = 'NUCYPHER_GRANT_METRICS_KEYFILE_PATH'
PROVIDER_ENVVAR: str = 'NUCYPHER_GRANT_METRICS_PROVIDER'
POLYGON_ENVVAR: str = 'NUCYPHER_GRANT_METRICS_POLYGON_PROVIDER'

try:
    ALICE_ADDRESS: ChecksumAddress = os.environ[ADDRESS_ENVVAR]
    SIGNER_PASSWORD: str = os.environ[PASSWORD_ENVVAR]
    SIGNER_URI: str = os.environ[SIGNER_ENVVAR]
    ETHEREUM_PROVIDER_URI: str = os.environ[PROVIDER_ENVVAR]
    POLYGON_PROVIDER_URI: str = os.environ[POLYGON_ENVVAR]
except KeyError:
    message = f'{ADDRESS_ENVVAR}, ' \
              f'{PROVIDER_ENVVAR}, ' \
              f'{SIGNER_ENVVAR} and ' \
              f'{PASSWORD_ENVVAR} ' \
              f'are required to run grant availability metrics.'
    raise RuntimeError(message)

# Alice Configuration
TACO_DOMAIN: str = str(LYNX)  # mainnet
DEFAULT_SEEDNODE_URIS: List[str] = [
    *TEACHER_NODES[TACO_DOMAIN],
]
INSECURE_PASSWORD: str = "METRICS_INSECURE_DEVELOPMENT_PASSWORD"
TEMP_ALICE_DIR: Path = Path('/', 'tmp', 'grant-metrics')

# Policy Parameters
THRESHOLD: int = 1
SHARES: int = 1
RATE: Wei = Web3.to_wei(50, 'gwei')
DURATION: datetime.timedelta = datetime.timedelta(days=1)

# Tuning
DEFAULT_ITERATIONS = None  # `None` will run forever
SAMPLE_RATE: int = 15  # seconds
GAS_STRATEGY: str = 'fast'
MAX_GAS_PRICE: int = 200  # gwei
LABEL_PREFIX = 'random-metrics-label-'
def LABEL_SUFFIXER():
    return os.urandom(16).hex()
HANDPICKED_URSULA_URIS: List[str] = [
    # DEFAULT_SEEDNODE_URIS[0],  # uncomment to use the seednode for granting
]


def make_random_bob():
    """Generates a random ephemeral Bob instance."""
    bob_verifying_secret = SecretKey.random()
    bob_verifying_key = bob_verifying_secret.public_key()
    decrypting_secret = SecretKey.random()
    decrypting_key = decrypting_secret.public_key()
    bob = Bob.from_public_keys(
        verifying_key=bob_verifying_key, encrypting_key=decrypting_key
    )
    print(f"Created BOB - {bytes(bob.stamp).hex()}")
    return bob


BOB = make_random_bob()


def metric_grant(alice, ursulas: Optional[Set[Ursula]] = None) -> Policy:
    """Perform a granting operation for metrics collection."""
    label = f'{LABEL_PREFIX}{LABEL_SUFFIXER()}'.encode()
    policy_end_datetime = maya.now() + DURATION
    policy = alice.grant(threshold=THRESHOLD,
                         shares=SHARES,
                         # ursulas=handpicked_ursulas,
                         expiration=policy_end_datetime,
                         bob=BOB,
                         label=label)
    return policy


def collect(alice: Alice,
            ursulas: Optional[Set[Ursula]] = None,
            iterations: Optional[int] = DEFAULT_ITERATIONS,
            ) -> None:
    """Collects grant success and failure rates."""
    policies, i, success, fail = dict(), 0, 0, 0
    while True:
        print(f"Attempt {i+1} of {iterations if iterations is not None else 'infinite'}")
        start = maya.now()
        try:
            policy = metric_grant(alice=alice, ursulas=ursulas)
        except Exception as e:
            fail += 1
            print(f'GRANT FAIL\n{e}')
        else:
            success += 1
            policies[policy.public_key.to_compressed_bytes().hex()] = policy  # track
            print(
                f"PEK:{policy.public_key.to_compressed_bytes().hex()} | {policy.hrac}"
            )

        # timeit
        end = maya.now()
        delta = end - start
        print(f"Completed in {(delta).total_seconds()} seconds.")

        # score
        if i+1 != iterations:
            if fail > 0:
                print(f'{fail}/{i+1} ({(fail/(i+1))*100}%) failure rate')
            if success > 0:
                print(f'{success}/{i+1} ({(success/(i+1))*100}%) success rate')
            print(f'Waiting {SAMPLE_RATE} seconds until next sample. ')
            time.sleep(SAMPLE_RATE)

        if i+1 == iterations:
            return  # exit
        else:
            i += 1  # continue


def make_alice(known_nodes: Optional[Set[Ursula]] = None):
    """Initializes a new 'persistent alice configuration' for grant metrics collection."""

    # This is Alice's PRE payment method.
    pre_payment_method = SubscriptionManagerPayment(
        domain=TACO_DOMAIN, blockchain_endpoint=POLYGON_PROVIDER_URI
    )

    wallet = Signer.from_signer_uri(f'keystore://{SIGNER_URI}')
    wallet.unlock_account(account=ALICE_ADDRESS, password=SIGNER_PASSWORD)

    alice_config = AliceConfiguration(
        eth_endpoint=ETHEREUM_PROVIDER_URI,
        polygon_endpoint=POLYGON_PROVIDER_URI,
        checksum_address=ALICE_ADDRESS,
        signer_uri=f'keystore://{SIGNER_URI}',
        config_root=TEMP_ALICE_DIR,
        domain=TACO_DOMAIN,
        known_nodes=known_nodes,
        start_learning_now=False,
        learn_on_same_thread=True,
        gas_strategy=GAS_STRATEGY,
        max_gas_price=MAX_GAS_PRICE,
    )

    alice_config.initialize(password=INSECURE_PASSWORD)
    alice_config.keystore.unlock(password=INSECURE_PASSWORD)
    alice = alice_config.produce(pre_payment_method=pre_payment_method, signer=wallet)
    alice.start_learning_loop(now=True)
    return alice


def setup():
    """Prepares the filesystem and logger for grant metrics collection"""
    shutil.rmtree(TEMP_ALICE_DIR, ignore_errors=True)
    GlobalLoggerSettings.start_console_logging()
    GlobalLoggerSettings.start_text_file_logging()
    GlobalLoggerSettings.set_log_level('info')


def aggregate_nodes(eth_endpoint: str) -> Tuple[Set[Ursula], Set[Ursula]]:
    """generates ursulas from URIs used in grant metrics collection"""

    seednodes = set()
    if DEFAULT_SEEDNODE_URIS:
        for uri in DEFAULT_SEEDNODE_URIS:
            ursula = Ursula.from_seed_and_stake_info(
                seed_uri=uri, eth_endpoint=eth_endpoint
            )
            seednodes.add(ursula)

    ursulas = set()
    if HANDPICKED_URSULA_URIS:
        for uri in HANDPICKED_URSULA_URIS:
            ursula = Ursula.from_seed_and_stake_info(
                seed_uri=uri, eth_endpoint=eth_endpoint
            )
            ursulas.add(ursula)

    return seednodes, ursulas


if __name__ == '__main__':
    setup()
    seednodes, ursulas = aggregate_nodes(eth_endpoint=ETHEREUM_PROVIDER_URI)
    alice = make_alice(known_nodes=seednodes)
    collect(alice=alice, ursulas=ursulas)
