# standard imports
import os
import logging

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

# local imports
from cic_eth.db import SessionBase
from cic_eth.db.models.convert import TxConvertTransfer
from cic_eth.db.models.otx import Otx
from cic_eth.db.models.tx import TxCache
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.token import TokenTxFactory
from cic_eth.eth.factory import TxFactory
from cic_eth.eth.util import unpack_signed_raw_tx
from cic_eth.eth.rpc import RpcClient

celery_app = celery.current_app 
#logg = celery_app.log.get_default_logger()
logg = logging.getLogger()

contract_function_signatures = {
        'convert': 'f3898a97',
        'convert2': '569706eb',
        }


class BancorTxFactory(TxFactory):

    """Factory for creating Bancor network transactions.
    """
    def convert(
            self,
            source_token_address,
            destination_token_address,
            reserve_address,
            source_amount,
            minimum_return,
            chain_spec,
            fee_beneficiary='0x0000000000000000000000000000000000000000',
            fee_ppm=0,
            ):
        """Create a BancorNetwork "convert" transaction.

        :param source_token_address: ERC20 contract address for token to convert from
        :type source_token_address: str, 0x-hex
        :param destination_token_address: ERC20 contract address for token to convert to
        :type destination_token_address: str, 0x-hex
        :param reserve_address: ERC20 contract address of Common reserve token
        :type reserve_address: str, 0x-hex
        :param source_amount: Amount of source tokens to convert
        :type source_amount: int
        :param minimum_return: Minimum amount of destination tokens to accept as result for conversion
        :type source_amount: int
        :return: Unsigned "convert" transaction in standard Ethereum format
        :rtype: dict
        """
        network_contract = CICRegistry.get_contract(chain_spec, 'BancorNetwork')
        network_gas = network_contract.gas('convert')
        tx_convert_buildable = network_contract.contract.functions.convert2(
            [
                source_token_address,
                source_token_address,
                reserve_address,
                destination_token_address,
                destination_token_address,
            ],
            source_amount,
            minimum_return,
            fee_beneficiary,
            fee_ppm,
            )
        tx_convert = tx_convert_buildable.buildTransaction({
                'from': self.address,
                'gas': network_gas,
                'gasPrice': self.gas_price,
                'chainId': chain_spec.chain_id(),
                'nonce': self.next_nonce(),
                })
        return tx_convert


def unpack_convert(data):
    f = data[2:10]
    if f != contract_function_signatures['convert2']:
        raise ValueError('Invalid convert data ({})'.format(f))

    d = data[10:]
    path = d[384:]
    source = path[64-40:64]
    destination = path[-40:]

    amount = int(d[64:128], 16)
    min_return = int(d[128:192], 16)
    fee_recipient = d[192:256]
    fee = int(d[256:320], 16)
    return {
        'amount': amount,
        'min_return': min_return,
        'source_token': web3.Web3.toChecksumAddress('0x' + source),
        'destination_token': web3.Web3.toChecksumAddress('0x' + destination),
        'fee_recipient': fee_recipient,
        'fee': fee,
        }



# Kept for historical reference, it unpacks a convert call without fee parameters
#def _unpack_convert_mint(data):
#    f = data[2:10]
#    if f != contract_function_signatures['convert2']:
#        raise ValueError('Invalid convert data ({})'.format(f))
#
#    d = data[10:]
#    path = d[256:]
#    source = path[64-40:64]
#    destination = path[-40:]
#
#    amount = int(d[64:128], 16)
#    min_return = int(d[128:192], 16)
#    return {
#        'amount': amount,
#        'min_return': min_return,
#        'source_token': web3.Web3.toChecksumAddress('0x' + source),
#        'destination_token': web3.Web3.toChecksumAddress('0x' + destination),
#        }


@celery_app.task(bind=True)
def convert_with_default_reserve(self, tokens, from_address, source_amount, minimum_return, to_address, chain_str):
    """Performs a conversion between two liquid tokens using Bancor network.

    :param tokens: Token pair, source and destination respectively
    :type tokens: list of str, 0x-hex
    :param from_address: Ethereum address of sender
    :type from_address: str, 0x-hex
    :param source_amount: Amount of source tokens to convert
    :type source_amount: int
    :param minimum_return: Minimum about of destination tokens to receive
    :type minimum_return: int
    """

    chain_spec = ChainSpec.from_chain_str(chain_str)
    queue = self.request.delivery_info['routing_key']

    c = RpcClient(chain_spec, holder_address=from_address)

    cr = CICRegistry.get_contract(chain_spec, 'BancorNetwork')
    source_token = CICRegistry.get_address(chain_spec, tokens[0]['address'])
    reserve_address = CICRegistry.get_contract(chain_spec, 'BNTToken', 'ERC20').address()

    tx_factory = TokenTxFactory(from_address, c)
   
    tx_approve_zero = tx_factory.approve(source_token.address(), cr.address(), 0, chain_spec)
    (tx_approve_zero_hash_hex, tx_approve_zero_signed_hex) = sign_and_register_tx(tx_approve_zero, chain_str, queue, 'cic_eth.eth.token.otx_cache_approve') 

    tx_approve = tx_factory.approve(source_token.address(), cr.address(), source_amount, chain_spec)
    (tx_approve_hash_hex, tx_approve_signed_hex) = sign_and_register_tx(tx_approve, chain_str, queue, 'cic_eth.eth.token.otx_cache_approve') 

    tx_factory = BancorTxFactory(from_address, c)
    tx_convert = tx_factory.convert(
            tokens[0]['address'],
            tokens[1]['address'],
            reserve_address,
            source_amount,
            minimum_return,
            chain_spec,
            )
    (tx_convert_hash_hex, tx_convert_signed_hex) = sign_and_register_tx(tx_convert, chain_str, queue, 'cic_eth.eth.bancor.otx_cache_convert')

    # TODO: consider moving save recipient to async task / chain it before the tx send
    if to_address != None:
        save_convert_recipient(tx_convert_hash_hex, to_address, chain_str)

    s = create_check_gas_and_send_task(
            [tx_approve_zero_signed_hex, tx_approve_signed_hex, tx_convert_signed_hex],
            chain_str,
            from_address,
            tx_approve_zero['gasPrice'] * tx_approve_zero['gas'],
            tx_hashes_hex=[tx_approve_hash_hex],
            queue=queue,
            )
    s.apply_async()
    return tx_convert_hash_hex


#@celery_app.task()
#def process_approval(tx_hash_hex):
#    t = session.query(TxConvertTransfer).query(TxConvertTransfer.approve_tx_hash==tx_hash_hex).first()
#    c = session.query(Otx).query(Otx.tx_hash==t.convert_tx_hash)
#    gas_limit = 8000000
#    gas_price = GasOracle.gas_price()
#
#    # TODO: use celery group instead
#    s_queue = celery.signature(
#            'cic_eth.queue.tx.create',
#            [
#                nonce,
#                c['address'], # TODO: check that this is in fact sender address
#                c['tx_hash'],
#                c['signed_tx'],
#                ]
#            )
#    s_queue.apply_async()
#
#    s_check_gas = celery.signature(
#            'cic_eth.eth.gas.check_gas',
#            [
#                c['address'],
#                [c['signed_tx']],
#                gas_limit * gas_price,
#                ]
#            )
#    s_send = celery.signature(
#            'cic_eth.eth.tx.send',
#            [],
#            )
#
#    s_set_sent = celery.signature(
#            'cic_eth.queue.state.set_sent',
#            [False],
#            )
#    s_send.link(s_set_sent)
#    s_check_gas.link(s_send)
#    s_check_gas.apply_async()
#    return tx_hash_hex



@celery_app.task()
def save_convert_recipient(convert_hash, recipient_address, chain_str):
    """Registers the recipient target for a convert-and-transfer operation.

    :param convert_hash: Transaction hash of convert operation
    :type convert_hash: str, 0x-hex
    :param recipient_address: Address of consequtive transfer recipient
    :type recipient_address: str, 0x-hex
    """
    session = SessionBase.create_session()
    t = TxConvertTransfer(convert_hash, recipient_address, chain_str)
    session.add(t)
    session.commit()
    session.close()


@celery_app.task()
def save_convert_transfer(convert_hash, transfer_hash):
    """Registers that the transfer part of a convert-and-transfer operation has been executed.

    :param convert_hash: Transaction hash of convert operation
    :type convert_hash: str, 0x-hex
    :param convert_hash: Transaction hash of transfer operation
    :type convert_hash: str, 0x-hex
    :returns: transfer_hash,
    :rtype: list, single str, 0x-hex
    """
    session = SessionBase.create_session()
    t = TxConvertTransfer.get(convert_hash)
    t.transfer(transfer_hash)
    session.add(t)
    session.commit()
    session.close()
    return [transfer_hash]


# TODO: seems unused, consider removing
@celery_app.task()
def resolve_converters_by_tokens(tokens, chain_str):
    """Return converters for a list of tokens.

    :param tokens: Token addresses to look up
    :type tokens: list of str, 0x-hex
    :return: Addresses of matching converters
    :rtype: list of str, 0x-hex
    """
    chain_spec = ChainSpec.from_chain_str(chain_str)
    for t in tokens:
        c = CICRegistry.get_contract(chain_spec, 'ConverterRegistry')
        fn = c.function('getConvertersByAnchors')
        try:
            converters = fn([t['address']]).call()
        except Exception as e:
            raise e
        t['converters'] = converters

    return tokens


@celery_app.task(bind=True)
def transfer_converted(self, tokens, holder_address, receiver_address, value, tx_convert_hash_hex, chain_str):
    """Execute the ERC20 transfer of a convert-and-transfer operation.

    First argument is a list of tokens, to enable the task to be chained to the symbol to token address resolver function. However, it accepts only one token as argument.

    :param tokens: Token addresses 
    :type tokens: list of str, 0x-hex
    :param holder_address: Token holder address
    :type holder_address: str, 0x-hex
    :param holder_address: Token receiver address
    :type holder_address: str, 0x-hex
    :param value: Amount of token, in 'wei'
    :type value: int
    :raises TokenCountError: Either none or more then one tokens have been passed as tokens argument
    :return: Transaction hash
    :rtype: str, 0x-hex
    """
    # we only allow one token, one transfer
    if len(tokens) != 1:
        raise TokenCountError

    chain_spec = ChainSpec.from_chain_str(chain_str)

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

    c = RpcClient(chain_spec, holder_address=holder_address)

    # get transaction parameters
    gas_price = c.gas_price()
    tx_factory = TokenTxFactory(holder_address, c)

    token_address = tokens[0]['address']
    tx_transfer = tx_factory.transfer(
        token_address,
        receiver_address,
        value,
        chain_spec,
            )
    (tx_transfer_hash_hex, tx_transfer_signed_hex) = sign_and_register_tx(tx_transfer, chain_str, queue, 'cic_eth.eth.token.otx_cache_transfer')

    # send transaction
    logg.info('transfer converted token {} from {} to {} value {} {}'.format(token_address, holder_address, receiver_address, value, tx_transfer_signed_hex))
    s = create_check_gas_and_send_task(
            [tx_transfer_signed_hex],
            chain_str,
            holder_address,
            tx_transfer['gasPrice'] * tx_transfer['gas'],
            None,
            queue,
            )
    s_save = celery.signature(
            'cic_eth.eth.bancor.save_convert_transfer',
            [
                tx_convert_hash_hex,
                tx_transfer_hash_hex,
                ],
            queue=queue,
            )
    s_save.link(s)
    s_save.apply_async()
    return tx_transfer_hash_hex


@celery_app.task()
def otx_cache_convert(
        tx_hash_hex,
        tx_signed_raw_hex,
        chain_str,
        ):

    chain_spec = ChainSpec.from_chain_str(chain_str)
    tx_signed_raw_bytes = bytes.fromhex(tx_signed_raw_hex[2:])
    tx = unpack(tx_signed_raw_bytes, chain_spec)
    tx_data = unpack_convert(tx['data'])
    logg.debug('tx data {}'.format(tx_data))

    session = TxCache.create_session()
    tx_cache = TxCache(
        tx_hash_hex,
        tx['from'],
        tx['from'],
        tx_data['source_token'],
        tx_data['destination_token'],
        tx_data['amount'],
        tx_data['amount'],
            )
    session.add(tx_cache)
    session.commit()
    session.close()
    return tx_hash_hex