# standard imports
import logging

# external imports
import celery
from chainlib.chain import ChainSpec
from chainlib.eth.address import is_checksum_address
from chainlib.eth.error import NotFoundEthException
from chainlib.eth.tx import (
        transaction,
        receipt,
        raw,
        )
from chainlib.connection import RPCConnection
from chainlib.hash import keccak256_hex_to_hex
from hexathon import (
        add_0x,
        strip_0x,
        )
from chainqueue.db.models.tx import Otx
from chainqueue.db.enum import StatusBits
from chainqueue.error import NotLocalTxError
from potaahto.symbols import snake_and_camel

# local imports
from cic_eth.db import SessionBase
from cic_eth.error import (
        PermanentTxError,
        TemporaryTxError,
        )
from cic_eth.eth.gas import create_check_gas_task
from cic_eth.admin.ctrl import lock_send
from cic_eth.task import (
        CriticalSQLAlchemyTask,
        CriticalWeb3Task,
        CriticalWeb3AndSignerTask,
        CriticalSQLAlchemyAndSignerTask,
        CriticalSQLAlchemyAndWeb3Task,
        )

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

MAX_NONCE_ATTEMPTS = 3


# TODO: chain chainable transactions that use hashes as inputs may be chained to this function to output signed txs instead.
@celery_app.task(bind=True, base=CriticalSQLAlchemyTask)
def hashes_to_txs(self, tx_hashes):
    """Return a list of raw signed transactions from the local transaction queue corresponding to a list of transaction hashes.

    :param tx_hashes: Transaction hashes
    :type tx_hashes: list of str, 0x-hex
    :raises ValueError: Empty input list
    :returns: Signed raw transactions
    :rtype: list of str, 0x-hex
    """
    if len(tx_hashes) == 0:
        raise ValueError('no transaction to send')

    for i in range(len(tx_hashes)):
        tx_hashes[i] = strip_0x(tx_hashes[i])

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

    session = SessionBase.create_session()
    q = session.query(Otx.signed_tx)
    q = q.filter(Otx.tx_hash.in_(tx_hashes))
    tx_tuples = q.all()
    session.close()

    def __head(x):
        return x[0]

    txs = []
    for f in map(__head, tx_tuples):
        txs.append(f)

    return txs


# TODO: A lock should be introduced to ensure that the send status change and the transaction send is atomic.
@celery_app.task(bind=True, base=CriticalWeb3Task)
def send(self, txs, chain_spec_dict):
    """Send transactions to the network.

    If more than one transaction is passed to the task, it will spawn a new send task with the remaining transaction(s) after the first in the list has been processed.

    Updates the outgoing transaction queue entry to SENT on successful send.

    If a temporary error occurs, the queue entry is set to SENDFAIL.

    If a permanent error occurs due to invalid transaction data, queue entry value is set to REJECTED.

    Any other permanent error that isn't explicitly handled will get value FUBAR.

    :param txs: Signed raw transaction data
    :type txs: list of str, 0x-hex
    :param chain_str: Chain spec, string representation
    :type chain_str: str
    :raises TemporaryTxError: If unable to connect to node
    :raises PermanentTxError: If EVM execution fails immediately due to tx input, or if tx contents are invalid. 
    :return: transaction hash of sent transaction
    :rtype: str, 0x-hex
    """
    if len(txs) == 0:
        raise ValueError('no transaction to send')

    chain_spec = ChainSpec.from_dict(chain_spec_dict)

    tx_hex = add_0x(txs[0])

    tx_hash_hex = add_0x(keccak256_hex_to_hex(tx_hex))

    logg.debug('send transaction {} -> {}'.format(tx_hash_hex, tx_hex))

    queue = self.request.delivery_info.get('routing_key')

    r = None
    s_set_sent = celery.signature(
        'cic_eth.queue.state.set_sent',
        [
            chain_spec_dict,
            tx_hash_hex,
            False
            ],
            queue=queue,
        )

    o = raw(tx_hex)
    conn = RPCConnection.connect(chain_spec, 'default')
    conn.do(o)

    s_set_sent.apply_async()

    tx_tail = txs[1:]
    if len(tx_tail) > 0:
        s = celery.signature(
            'cic_eth.eth.tx.send',
            [
                tx_tail,
                chain_spec_dict,
            ],
            queue=queue,
                )
        s.apply_async()

    return tx_hash_hex



@celery_app.task(bind=True, throws=(NotFoundEthException,), base=CriticalWeb3Task)
def sync_tx(self, tx_hash_hex, chain_spec_dict):
    """Force update of network status of a single transaction

    :param tx_hash_hex: Transaction hash
    :type tx_hash_hex: str, 0x-hex
    :param chain_str: Chain spec string representation
    :type chain_str: str
    """

    queue = self.request.delivery_info.get('routing_key')

    chain_spec = ChainSpec.from_dict(chain_spec_dict)

    conn = RPCConnection.connect(chain_spec, 'default')
    o = transaction(tx_hash_hex)
    tx = conn.do(o)

    rcpt = None
    try:
        o = receipt(tx_hash_hex)
        rcpt = conn.do(o)
    except NotFoundEthException as e:
        pass

    # TODO: apply receipt in tx object to validate and normalize input
    if rcpt != None:
        rcpt = snake_and_camel(rcpt)
        success = rcpt['status'] == 1
        logg.debug('sync tx {} mined block {} tx index {} success {}'.format(tx_hash_hex, rcpt['blockNumber'], rcpt['transactionIndex'], success))

        s = celery.signature(
            'cic_eth.queue.state.set_final',
            [
                chain_spec_dict,
                tx_hash_hex,
                rcpt['blockNumber'],
                rcpt['transactionIndex'],
                not success,
                ],
                queue=queue,
            )
    # TODO: it's not entirely clear how we can reliable determine that its in mempool without explicitly checking
    else:
        logg.debug('sync tx {} mempool'.format(tx_hash_hex))

        s = celery.signature(
            'cic_eth.queue.state.set_sent',
            [
                chain_spec_dict,
                tx_hash_hex,
                ],
                queue=queue,
            )

    s.apply_async()


#
#@celery_app.task(bind=True)
#def resume_tx(self, txpending_hash_hex, chain_str):
#    """Queue a suspended tranaction for (re)sending
#
#    :param txpending_hash_hex: Transaction hash
#    :type txpending_hash_hex: str, 0x-hex
#    :param chain_str: Chain spec, string representation
#    :type chain_str: str
#    :raises NotLocalTxError: Transaction does not exist in the local queue
#    :returns: Transaction hash
#    :rtype: str, 0x-hex
#    """
#
#    chain_spec = ChainSpec.from_chain_str(chain_str)
#
#    session = SessionBase.create_session()
#    q = session.query(Otx.signed_tx)
#    q = q.filter(Otx.tx_hash==txpending_hash_hex)
#    r = q.first()
#    session.close()
#    if r == None:
#        raise NotLocalTxError(txpending_hash_hex)
#
#    tx_signed_raw_hex = r[0]
#    tx_signed_bytes = bytes.fromhex(tx_signed_raw_hex[2:])
#    tx = unpack(tx_signed_bytes, chain_spec)
#
#    queue = self.request.delivery_info['routing_key']
#
#    s = create_check_gas_and_send_task(
#            [tx_signed_raw_hex],
#            chain_str,
#            tx['from'],
#            tx['gasPrice'] * tx['gas'],
#            [txpending_hash_hex],
#            queue=queue,
#            )
#    s.apply_async()
#    return txpending_hash_hex