# standard imports

import json
import logging
from typing import Union, Optional

# third-party imports
from cic_eth.api import Api
from cic_eth_aux.erc20_demurrage_token.api import Api as DemurrageApi
from cic_types.condiments import MetadataPointer

# local imports
from cic_ussd.account.transaction import from_wei
from cic_ussd.cache import cache_data_key, get_cached_data
from cic_ussd.error import CachedDataNotFoundError

logg = logging.getLogger(__file__)


def get_balances(address: str,
                 chain_str: str,
                 token_symbol: str,
                 asynchronous: bool = False,
                 callback_param: any = None,
                 callback_queue='cic-ussd',
                 callback_task='cic_ussd.tasks.callback_handler.balances_callback') -> Optional[list]:
    """This function queries cic-eth for an account's balances, It provides a means to receive the balance either
    asynchronously or synchronously.. It returns a dictionary containing the network, outgoing and incoming balances.
    :param address: Ethereum address of an account.
    :type address: str, 0x-hex
    :param chain_str: The chain name and network id.
    :type chain_str: str
    :param asynchronous: Boolean value checking whether to return balances asynchronously.
    :type asynchronous: bool
    :param callback_param: Data to be sent along with the callback containing balance data.
    :type callback_param: any
    :param callback_queue:
    :type callback_queue:
    :param callback_task: A celery task path to which callback data should be sent.
    :type callback_task: str
    :param token_symbol: ERC20 token symbol of the account whose balance is being queried.
    :type token_symbol: str
    :return: A list containing balance data if called synchronously. | None
    :rtype: list | None
    """
    logg.debug(f'retrieving {token_symbol} balance for address: {address}')
    if asynchronous:
        cic_eth_api = Api(
            chain_str=chain_str,
            callback_queue=callback_queue,
            callback_task=callback_task,
            callback_param=callback_param
        )
        cic_eth_api.balance(address=address, token_symbol=token_symbol)
    else:
        cic_eth_api = Api(chain_str=chain_str)
        balance_request_task = cic_eth_api.balance(
            address=address,
            token_symbol=token_symbol)
        return balance_request_task.get()


def calculate_available_balance(balances: dict, decimals: int) -> float:
    """This function calculates an account's balance at a specific point in time by computing the difference from the
    outgoing balance and the sum of the incoming and network balances.
    :param balances: incoming, network and outgoing balances.
    :type balances: dict
    :param decimals:
    :type decimals: int
    :return: Token value of the available balance.
    :rtype: float
    """
    incoming_balance = balances.get('balance_incoming')
    outgoing_balance = balances.get('balance_outgoing')
    network_balance = balances.get('balance_network')

    available_balance = (network_balance + incoming_balance) - outgoing_balance
    return from_wei(decimals=decimals, value=available_balance)


def get_adjusted_balance(balance: int, chain_str: str, timestamp: int, token_symbol: str):
    """
    :param balance:
    :type balance:
    :param chain_str:
    :type chain_str:
    :param timestamp:
    :type timestamp:
    :param token_symbol:
    :type token_symbol:
    :return:
    :rtype:
    """
    logg.debug(f'retrieving adjusted balance on chain: {chain_str}')
    demurrage_api = DemurrageApi(chain_str=chain_str)
    return demurrage_api.get_adjusted_balance(token_symbol, balance, timestamp).result


def get_cached_available_balance(decimals: int, identifier: Union[list, bytes]) -> float:
    """This function attempts to retrieve balance data from the redis cache.
    :param decimals:
    :type decimals: int
    :param identifier: An identifier needed to create a unique pointer to a balances resource.
    :type identifier: bytes | list
    :raises CachedDataNotFoundError: No cached balance data could be found.
    :return: Operational balance of an account.
    :rtype: float
    """
    key = cache_data_key(identifier=identifier, salt=MetadataPointer.BALANCES)
    cached_balances = get_cached_data(key=key)
    if cached_balances:
        return calculate_available_balance(balances=json.loads(cached_balances), decimals=decimals)
    else:
        raise CachedDataNotFoundError(f'No cached available balance at {key}')


def get_cached_adjusted_balance(identifier: Union[list, bytes]):
    """
    :param identifier:
    :type identifier:
    :return:
    :rtype:
    """
    key = cache_data_key(identifier, MetadataPointer.BALANCES_ADJUSTED)
    return get_cached_data(key)


def get_account_tokens_balance(blockchain_address: str, chain_str: str, token_symbols_list: list):
    """
    :param blockchain_address:
    :type blockchain_address:
    :param chain_str:
    :type chain_str:
    :param token_symbols_list:
    :type token_symbols_list:
    :return:
    :rtype:
    """
    for token_symbol in token_symbols_list:
        get_balances(address=blockchain_address,
                     chain_str=chain_str,
                     token_symbol=token_symbol,
                     asynchronous=True,
                     callback_param=f'{blockchain_address},{token_symbol}')