# standard imports
import logging

# third-party imports
import web3
import celery
from cic_registry import CICRegistry
from cic_registry.chain import ChainSpec
from erc20_single_shot_faucet import Faucet
from cic_registry import zero_address
from hexathon import strip_0x

# local import
from cic_eth.eth import RpcClient
from cic_eth.eth import registry_extra_identifiers
from cic_eth.eth.task import sign_and_register_tx
from cic_eth.eth.task import create_check_gas_and_send_task
from cic_eth.eth.factory import TxFactory
from cic_eth.db.models.nonce import Nonce
from cic_eth.db.models.base import SessionBase
from cic_eth.db.models.role import AccountRole
from cic_eth.db.models.tx import TxCache
from cic_eth.eth.util import unpack_signed_raw_tx
from cic_eth.error import RoleMissingError
from cic_eth.task import CriticalSQLAlchemyTask

#logg = logging.getLogger(__name__)
logg = logging.getLogger()
celery_app = celery.current_app 


class AccountTxFactory(TxFactory):
    """Factory for creating account index contract transactions
    """
    def add(
            self,
            address,
            chain_spec,
            session=None,
            ):
        """Register an Ethereum account address with the on-chain account registry

        :param address: Ethereum account address to add
        :type address: str, 0x-hex
        :param chain_spec: Chain to build transaction for
        :type chain_spec: cic_registry.chain.ChainSpec
        :returns: Unsigned "AccountRegistry.add" transaction in standard Ethereum format
        :rtype: dict
        """

        c = CICRegistry.get_contract(chain_spec, 'AccountRegistry')
        f = c.function('add')
        tx_add_buildable = f(
                address,
                )
        gas = c.gas('add')
        tx_add = tx_add_buildable.buildTransaction({
            'from': self.address,
            'gas': gas,
            'gasPrice': self.gas_price,
            'chainId': chain_spec.chain_id(),
            'nonce': self.next_nonce(session=session),
            'value': 0,
            })
        return tx_add


    def gift(
            self,
            address,
            chain_spec,
            session=None,
        ):
        """Trigger the on-chain faucet to disburse tokens to the provided Ethereum account

        :param address: Ethereum account address to gift to
        :type address: str, 0x-hex
        :param chain_spec: Chain to build transaction for
        :type chain_spec: cic_registry.chain.ChainSpec
        :returns: Unsigned "Faucet.giveTo" transaction in standard Ethereum format
        :rtype: dict
        """

        c = CICRegistry.get_contract(chain_spec, 'Faucet')
        f = c.function('giveTo')
        tx_add_buildable = f(address)
        gas = c.gas('add')
        tx_add = tx_add_buildable.buildTransaction({
            'from': self.address,
            'gas': gas,
            'gasPrice': self.gas_price,
            'chainId': chain_spec.chain_id(),
            'nonce': self.next_nonce(session=session),
            'value': 0,
            })
        return tx_add


def unpack_register(data):
    """Verifies that a transaction is an "AccountRegister.add" transaction, and extracts call parameters from it.

    :param data: Raw input data from Ethereum transaction.
    :type data: str, 0x-hex
    :raises ValueError: Function signature does not match AccountRegister.add
    :returns: Parsed parameters
    :rtype: dict
    """
    data = strip_0x(data)
    f = data[:8]
    if f != '0a3b0a4f':
        raise ValueError('Invalid account index register data ({})'.format(f))

    d = data[8:]
    return {
        'to': web3.Web3.toChecksumAddress('0x' + d[64-40:64]),
        }


def unpack_gift(data):
    """Verifies that a transaction is a "Faucet.giveTo" transaction, and extracts call parameters from it.

    :param data: Raw input data from Ethereum transaction.
    :type data: str, 0x-hex
    :raises ValueError: Function signature does not match AccountRegister.add
    :returns: Parsed parameters
    :rtype: dict
    """
    data = strip_0x(data)
    f = data[:8]
    if f != '63e4bff4':
        raise ValueError('Invalid gift data ({})'.format(f))

    d = data[8:]
    return {
        'to': web3.Web3.toChecksumAddress('0x' + d[64-40:64]),
        }
     

# TODO: Separate out nonce initialization task
@celery_app.task(base=CriticalSQLAlchemyTask)
def create(password, chain_str):
    """Creates and stores a new ethereum account in the keystore.

    The password is passed on to the wallet backend, no encryption is performed in the task worker.

    :param password: Password to encrypt private key with
    :type password: str
    :param chain_str: Chain spec string representation
    :type chain_str: str
    :returns: Ethereum address of newly created account
    :rtype: str, 0x-hex
    """
    chain_spec = ChainSpec.from_chain_str(chain_str)
    c = RpcClient(chain_spec)
    a = c.w3.eth.personal.new_account(password)
    logg.debug('created account {}'.format(a))

    # Initialize nonce provider record for account
    # TODO: this can safely be set to zero, since we are randomly creating account
    n = c.w3.eth.getTransactionCount(a, 'pending')
    session = SessionBase.create_session()
    q = session.query(Nonce)
    q = q.filter(Nonce.address_hex==a)
    o = q.first()
    session.flush()
    if o == None:
        o = Nonce()
        o.address_hex = a
        o.nonce = n
        session.add(o)
        session.commit()
    session.close()
    return a


@celery_app.task(bind=True, throws=(RoleMissingError,), base=CriticalSQLAlchemyTask)
def register(self, account_address, chain_str, writer_address=None):
    """Creates a transaction to add the given address to the accounts index.

    :param account_address: Ethereum address to add
    :type account_address: str, 0x-hex
    :param chain_str: Chain spec string representation
    :type chain_str: str
    :param writer_address: Specify address in keystore to sign transaction. Overrides local accounts role setting.
    :type writer_address: str, 0x-hex
    :raises RoleMissingError: Writer address not set and writer role not found.
    :returns: The account_address input param
    :rtype: str, 0x-hex
    """
    chain_spec = ChainSpec.from_chain_str(chain_str)

    session = SessionBase.create_session()
    if writer_address == None:
        writer_address = AccountRole.get_address('ACCOUNTS_INDEX_WRITER', session=session)

    if writer_address == zero_address:
        session.close()
        raise RoleMissingError(account_address)

    logg.debug('adding account address {} to index; writer {}'.format(account_address, writer_address))
    queue = self.request.delivery_info['routing_key']

    c = RpcClient(chain_spec, holder_address=writer_address)
    txf = AccountTxFactory(writer_address, c)

    tx_add = txf.add(account_address, chain_spec, session=session)
    session.close()
    (tx_hash_hex, tx_signed_raw_hex) = sign_and_register_tx(tx_add, chain_str, queue, 'cic_eth.eth.account.cache_account_data')

    gas_budget = tx_add['gas'] * tx_add['gasPrice']

    logg.debug('register user tx {}'.format(tx_hash_hex))
    s = create_check_gas_and_send_task(
            [tx_signed_raw_hex],
            chain_str,
            writer_address,
            gas_budget,
            tx_hashes_hex=[tx_hash_hex],
            queue=queue,
            )
    s.apply_async()
    return account_address


@celery_app.task(bind=True, base=CriticalSQLAlchemyTask)
def gift(self, account_address, chain_str):
    """Creates a transaction to invoke the faucet contract for the given address.

    :param account_address: Ethereum address to give to
    :type account_address: str, 0x-hex
    :param chain_str: Chain spec string representation
    :type chain_str: str
    :returns: Raw signed transaction
    :rtype: list with transaction as only element
    """
    chain_spec = ChainSpec.from_chain_str(chain_str)

    logg.debug('gift account address {} to index'.format(account_address))
    queue = self.request.delivery_info['routing_key']

    c = RpcClient(chain_spec, holder_address=account_address)
    txf = AccountTxFactory(account_address, c)

    tx_add = txf.gift(account_address, chain_spec)
    (tx_hash_hex, tx_signed_raw_hex) = sign_and_register_tx(tx_add, chain_str, queue, 'cic_eth.eth.account.cache_gift_data')

    gas_budget = tx_add['gas'] * tx_add['gasPrice']

    logg.debug('register user tx {}'.format(tx_hash_hex))
    s = create_check_gas_and_send_task(
            [tx_signed_raw_hex],
            chain_str,
            account_address,
            gas_budget,
            [tx_hash_hex],
            queue=queue,
            )
    s.apply_async()
    return [tx_signed_raw_hex]


@celery_app.task(bind=True)
def have(self, account, chain_str):
    """Check whether the given account exists in keystore

    :param account: Account to check
    :type account: str, 0x-hex
    :param chain_str: Chain spec string representation
    :type chain_str: str
    :returns: Account, or None if not exists
    :rtype: Varies
    """
    c = RpcClient(account)
    try:
        c.w3.eth.sign(account, text='2a')
        return account
    except Exception as e:
        logg.debug('cannot sign with {}: {}'.format(account, e))
        return None


@celery_app.task(bind=True)
def role(self, account, chain_str):
    """Return account role for address

    :param account: Account to check
    :type account: str, 0x-hex
    :param chain_str: Chain spec string representation
    :type chain_str: str
    :returns: Account, or None if not exists
    :rtype: Varies
    """
    return AccountRole.role_for(account)


@celery_app.task()
def cache_gift_data(
    tx_hash_hex,
    tx_signed_raw_hex,
    chain_str,
        ):
    """Generates and commits transaction cache metadata for a Faucet.giveTo transaction

    :param tx_hash_hex: Transaction hash
    :type tx_hash_hex: str, 0x-hex
    :param tx_signed_raw_hex: Raw signed transaction
    :type tx_signed_raw_hex: str, 0x-hex
    :param chain_str: Chain spec string representation
    :type chain_str: str
    :returns: Transaction hash and id of cache element in storage backend, respectively
    :rtype: tuple
    """
    chain_spec = ChainSpec.from_chain_str(chain_str)
    c = RpcClient(chain_spec)

    tx_signed_raw_bytes = bytes.fromhex(tx_signed_raw_hex[2:])
    tx = unpack_signed_raw_tx(tx_signed_raw_bytes, chain_spec.chain_id())
    tx_data = unpack_gift(tx['data'])

    session = SessionBase.create_session()

    tx_cache = TxCache(
        tx_hash_hex,
        tx['from'],
        tx['to'],
        zero_address,
        zero_address,
        0,
        0,
        session=session,
            )

    session.add(tx_cache)
    session.commit()
    cache_id = tx_cache.id
    session.close()
    return (tx_hash_hex, cache_id)


@celery_app.task(base=CriticalSQLAlchemyTask)
def cache_account_data(
    tx_hash_hex,
    tx_signed_raw_hex,
    chain_str,
        ):
    """Generates and commits transaction cache metadata for an AccountsIndex.add  transaction

    :param tx_hash_hex: Transaction hash
    :type tx_hash_hex: str, 0x-hex
    :param tx_signed_raw_hex: Raw signed transaction
    :type tx_signed_raw_hex: str, 0x-hex
    :param chain_str: Chain spec string representation
    :type chain_str: str
    :returns: Transaction hash and id of cache element in storage backend, respectively
    :rtype: tuple
    """

    chain_spec = ChainSpec.from_chain_str(chain_str)
    c = RpcClient(chain_spec)

    tx_signed_raw_bytes = bytes.fromhex(tx_signed_raw_hex[2:])
    tx = unpack_signed_raw_tx(tx_signed_raw_bytes, chain_spec.chain_id())
    tx_data = unpack_register(tx['data'])

    session = SessionBase.create_session()
    tx_cache = TxCache(
        tx_hash_hex,
        tx['from'],
        tx['to'],
        zero_address,
        zero_address,
        0,
        0,
        session=session,
            )
    session.add(tx_cache)
    session.commit()
    cache_id = tx_cache.id
    session.close()
    return (tx_hash_hex, cache_id)
