# standard imports
import logging

# third-party imports
import web3
import celery
from erc20_approval_escrow import TransferApproval
from cic_registry import CICRegistry
from cic_registry.chain import ChainSpec

# local imports
from cic_eth.db.models.tx import TxCache
from cic_eth.db.models.base import SessionBase
from cic_eth.eth import RpcClient
from cic_eth.eth.factory import TxFactory
from cic_eth.eth.task import sign_and_register_tx
from cic_eth.eth.util import unpack_signed_raw_tx
from cic_eth.eth.task import create_check_gas_and_send_task
from cic_eth.error import TokenCountError

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

contract_function_signatures = {
        'request': 'b0addede',
        }


class TransferRequestTxFactory(TxFactory):
    """Factory for creating Transfer request transactions using the TransferApproval contract backend
    """
    def request(
            self,
            token_address,
            beneficiary_address,
            amount,
            chain_spec,
            ):
        """Create a new TransferApproval.request transaction

        :param token_address: Token to create transfer request for
        :type token_address: str, 0x-hex
        :param beneficiary_address: Beneficiary of token transfer
        :type beneficiary_address: str, 0x-hex
        :param amount: Amount of tokens to transfer
        :type amount: number
        :param chain_spec: Chain spec
        :type chain_spec: cic_registry.chain.ChainSpec
        :returns: Transaction in standard Ethereum format
        :rtype: dict
        """
        transfer_approval = CICRegistry.get_contract(chain_spec, 'TransferApproval', 'TransferAuthorization')
        fn = transfer_approval.function('createRequest')
        tx_approval_buildable = fn(beneficiary_address, token_address, amount)
        transfer_approval_gas = transfer_approval.gas('createRequest')

        tx_approval = tx_approval_buildable.buildTransaction({
            'from': self.address,
            'gas': transfer_approval_gas,
            'gasPrice': self.gas_price,
            'chainId': chain_spec.chain_id(),
            'nonce': self.next_nonce(),
            })
        return tx_approval


def unpack_transfer_approval_request(data):
    """Verifies that a transaction is an "TransferApproval.request" 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
    """
    f = data[2:10]
    if f != contract_function_signatures['request']:
        raise ValueError('Invalid transfer request data ({})'.format(f))

    d = data[10:]
    return {
        'to': web3.Web3.toChecksumAddress('0x' + d[64-40:64]),
        'token': web3.Web3.toChecksumAddress('0x' + d[128-40:128]),
        'amount': int(d[128:], 16)
        }


@celery_app.task(bind=True)
def transfer_approval_request(self, tokens, holder_address, receiver_address, value, chain_str):
    """Creates a new transfer approval

    :param tokens: Token to generate transfer request for
    :type tokens: list with single token spec as dict
    :param holder_address: Address to generate transfer on behalf of
    :type holder_address: str, 0x-hex
    :param receiver_address: Address to transfser tokens to
    :type receiver_address: str, 0x-hex
    :param value: Amount of tokens to transfer
    :type value: number
    :param chain_spec: Chain spec string representation
    :type chain_spec: str
    :raises cic_eth.error.TokenCountError: More than one token in tokens argument
    :returns: Raw signed transaction
    :rtype: list with transaction as only element
    """

    if len(tokens) != 1:
        raise TokenCountError

    chain_spec = ChainSpec.from_chain_str(chain_str)

    queue = self.request.delivery_info['routing_key']

    t = tokens[0]

    c = RpcClient(holder_address)

    txf = TransferRequestTxFactory(holder_address, c)

    tx_transfer = txf.request(t['address'], receiver_address, value, chain_spec)
    (tx_hash_hex, tx_signed_raw_hex) = sign_and_register_tx(tx_transfer, chain_str, queue, 'cic_eth.eth.request.otx_cache_transfer_approval_request')

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

    s = create_check_gas_and_send_task(
             [tx_signed_raw_hex],
             chain_str,
             holder_address,
             gas_budget,
             [tx_hash_hex],
             queue,
            )
    s.apply_async()
    return [tx_signed_raw_hex]


@celery_app.task()
def otx_cache_transfer_approval_request(
        tx_hash_hex,
        tx_signed_raw_hex,
        chain_str,
        ):
    """Generates and commits transaction cache metadata for an TransferApproval.request 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)
    tx_signed_raw_bytes = bytes.fromhex(tx_signed_raw_hex[2:])
    tx = unpack_signed_raw_tx(tx_signed_raw_bytes, chain_spec.chain_id())
    logg.debug('in otx acche transfer approval request')
    (txc, cache_id) = cache_transfer_approval_request_data(tx_hash_hex, tx)
    return txc


@celery_app.task()
def cache_transfer_approval_request_data(
    tx_hash_hex,
    tx,
        ):
    """Helper function for otx_cache_transfer_approval_request

    :param tx_hash_hex: Transaction hash
    :type tx_hash_hex: str, 0x-hex
    :param tx: Signed raw transaction
    :type tx: str, 0x-hex
    :returns: Transaction hash and id of cache element in storage backend, respectively
    :rtype: tuple
    """
    tx_data = unpack_transfer_approval_request(tx['data'])
    logg.debug('tx approval request data {}'.format(tx_data))
    logg.debug('tx approval request {}'.format(tx))

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