# standard imports
import hashlib
import json
import logging
from typing import Optional, Union

# external imports
from cic_eth.api import Api
from cic_types.condiments import MetadataPointer

# local imports
from cic_ussd.account.balance import get_cached_available_balance
from cic_ussd.account.chain import Chain
from cic_ussd.cache import cache_data, cache_data_key, get_cached_data
from cic_ussd.error import CachedDataNotFoundError, SeppukuError
from cic_ussd.metadata.tokens import query_token_info, query_token_metadata
from cic_ussd.processor.util import wait_for_cache
from cic_ussd.translation import translation_for

logg = logging.getLogger(__file__)


def collate_token_metadata(token_info: dict, token_metadata: dict) -> dict:
    """
    :param token_info:
    :type token_info:
    :param token_metadata:
    :type token_metadata:
    :return:
    :rtype:
    """
    logg.debug(f'Collating token info: {token_info} and token metadata: {token_metadata}')
    description = token_info.get('description')
    issuer = token_info.get('issuer')
    location = token_metadata.get('location')
    contact = token_metadata.get('contact')
    return {
        'description': description,
        'issuer': issuer,
        'location': location,
        'contact': contact
    }


def create_account_tokens_list(blockchain_address: str):
    """
    :param blockchain_address:
    :type blockchain_address:
    :return:
    :rtype:
    """
    token_symbols_list = get_cached_token_symbol_list(blockchain_address=blockchain_address)
    token_list_entries = []
    if token_symbols_list:
        logg.debug(f'Token symbols: {token_symbols_list} for account: {blockchain_address}')
        for token_symbol in token_symbols_list:
            entry = {}
            logg.debug(f'Processing token data for: {token_symbol}')
            key = cache_data_key([bytes.fromhex(blockchain_address), token_symbol.encode('utf-8')], MetadataPointer.TOKEN_DATA)
            token_data = get_cached_data(key)
            token_data = json.loads(token_data)
            logg.debug(f'Retrieved token data: {token_data} for: {token_symbol}')
            token_name = token_data.get('name')
            entry['name'] = token_name
            token_symbol = token_data.get('symbol')
            entry['symbol'] = token_symbol
            token_issuer = token_data.get('issuer')
            entry['issuer'] = token_issuer
            token_contact = token_data['contact'].get('phone')
            entry['contact'] = token_contact
            token_location = token_data.get('location')
            entry['location'] = token_location
            decimals = token_data.get('decimals')
            identifier = [bytes.fromhex(blockchain_address), token_symbol.encode('utf-8')]
            wait_for_cache(identifier, f'Cached available balance for token: {token_symbol}', MetadataPointer.BALANCES)
            token_balance = get_cached_available_balance(decimals=decimals, identifier=identifier)
            entry['balance'] = token_balance
            token_list_entries.append(entry)
    account_tokens_list = order_account_tokens_list(token_list_entries, bytes.fromhex(blockchain_address))
    key = cache_data_key(bytes.fromhex(blockchain_address), MetadataPointer.TOKEN_DATA_LIST)
    cache_data(key, json.dumps(account_tokens_list))


def get_active_token_symbol(blockchain_address: str):
    """
    :param blockchain_address:
    :type blockchain_address:
    :return:
    :rtype:
    """
    identifier = bytes.fromhex(blockchain_address)
    key = cache_data_key(identifier=identifier, salt=MetadataPointer.TOKEN_ACTIVE)
    active_token_symbol = get_cached_data(key)
    if not active_token_symbol:
        raise CachedDataNotFoundError('No active token set.')
    return active_token_symbol


def get_cached_token_data(blockchain_address: str, token_symbol: str):
    """
    :param blockchain_address:
    :type blockchain_address:
    :param token_symbol:
    :type token_symbol:
    :return:
    :rtype:
    """
    identifier = [bytes.fromhex(blockchain_address), token_symbol.encode('utf-8')]
    key = cache_data_key(identifier, MetadataPointer.TOKEN_DATA)
    logg.debug(f'Retrieving token data for: {token_symbol} at: {key}')
    token_data = get_cached_data(key)
    return json.loads(token_data)


def get_cached_default_token(chain_str: str) -> Optional[str]:
    """This function attempts to retrieve the default token's data from the redis cache.
    :param chain_str: chain name and network id.
    :type chain_str: str
    :return:
    :rtype:
    """
    logg.debug(f'Retrieving default token from cache for chain: {chain_str}')
    key = cache_data_key(identifier=chain_str.encode('utf-8'), salt=MetadataPointer.TOKEN_DEFAULT)
    return get_cached_data(key=key)


def get_default_token_symbol():
    """This function attempts to retrieve the default token's symbol from cached default token's data.
    :raises SeppukuError: The system should terminate itself because the default token is required for an appropriate
    system state.
    :return: Default token's symbol.
    :rtype: str
    """
    chain_str = Chain.spec.__str__()
    cached_default_token = get_cached_default_token(chain_str)
    if cached_default_token:
        default_token_data = json.loads(cached_default_token)
        return default_token_data.get('symbol')
    else:
        logg.warning('Cached default token data not found. Attempting retrieval from default token API')
        default_token_data = query_default_token(chain_str)
        if default_token_data:
            return default_token_data.get('symbol')
        else:
            raise SeppukuError(f'Could not retrieve default token for: {chain_str}')


def get_cached_token_symbol_list(blockchain_address: str) -> Optional[list]:
    """
    :param blockchain_address:
    :type blockchain_address:
    :return:
    :rtype:
    """
    key = cache_data_key(identifier=bytes.fromhex(blockchain_address), salt=MetadataPointer.TOKEN_SYMBOLS_LIST)
    token_symbols_list = get_cached_data(key)
    if token_symbols_list:
        return json.loads(token_symbols_list)
    return token_symbols_list


def get_cached_token_data_list(blockchain_address: str) -> Optional[list]:
    """
    :param blockchain_address:
    :type blockchain_address:
    :return:
    :rtype:
    """
    key = cache_data_key(bytes.fromhex(blockchain_address), MetadataPointer.TOKEN_DATA_LIST)
    token_data_list = get_cached_data(key)
    if token_data_list:
        return json.loads(token_data_list)
    return token_data_list


def handle_token_symbol_list(blockchain_address: str, token_symbol: str):
    """
    :param blockchain_address:
    :type blockchain_address:
    :param token_symbol:
    :type token_symbol:
    :return:
    :rtype:
    """
    token_symbol_list = get_cached_token_symbol_list(blockchain_address)
    if token_symbol_list:
        if token_symbol not in token_symbol_list:
            token_symbol_list.append(token_symbol)
    else:
        token_symbol_list = [token_symbol]

    identifier = bytes.fromhex(blockchain_address)
    key = cache_data_key(identifier=identifier, salt=MetadataPointer.TOKEN_SYMBOLS_LIST)
    data = json.dumps(token_symbol_list)
    cache_data(key, data)


def hashed_token_proof(token_proof: Union[dict, str]) -> str:
    """
    :param token_proof:
    :type token_proof:
    :return:
    :rtype:
    """
    if isinstance(token_proof, dict):
        token_proof = json.dumps(token_proof)
    logg.debug(f'Hashing token proof: {token_proof}')
    hash_object = hashlib.new("sha256")
    hash_object.update(token_proof.encode('utf-8'))
    return hash_object.digest().hex()


def order_account_tokens_list(account_tokens_list: list, identifier: bytes) -> list:
    """
    :param account_tokens_list:
    :type account_tokens_list:
    :param identifier:
    :type identifier:
    :return:
    :rtype:
    """
    ordered_tokens_list = []
    # get last sent token
    key = cache_data_key(identifier=identifier, salt=MetadataPointer.TOKEN_LAST_SENT)
    last_sent_token_symbol = get_cached_data(key)

    # get last received token
    key = cache_data_key(identifier=identifier, salt=MetadataPointer.TOKEN_LAST_RECEIVED)
    last_received_token_symbol = get_cached_data(key)

    last_sent_token_data, remaining_accounts_token_list = remove_from_account_tokens_list(account_tokens_list, last_sent_token_symbol)
    if last_sent_token_data:
        ordered_tokens_list.append(last_sent_token_data[0])

    last_received_token_data, remaining_accounts_token_list = remove_from_account_tokens_list(remaining_accounts_token_list, last_received_token_symbol)
    if last_received_token_data:
        ordered_tokens_list.append(last_received_token_data[0])

    # order the by balance
    ordered_by_balance = sorted(remaining_accounts_token_list, key=lambda d: d['balance'], reverse=True)
    return ordered_tokens_list + ordered_by_balance


def parse_token_list(account_token_list: list):
    parsed_token_list = []
    for i in range(len(account_token_list)):
        token_symbol = account_token_list[i].get('symbol')
        token_balance = account_token_list[i].get('balance')
        token_data_repr = f'{i+1}. {token_symbol} {token_balance}'
        parsed_token_list.append(token_data_repr)
    return parsed_token_list


def process_token_data(blockchain_address: str, token_symbol: str):
    """

    :param blockchain_address:
    :type blockchain_address:
    :param token_symbol:
    :type token_symbol:
    :return:
    :rtype:
    """
    logg.debug(f'Processing token data for token: {token_symbol}')
    identifier = token_symbol.encode('utf-8')
    query_token_metadata(identifier=identifier)
    token_info = query_token_info(identifier=identifier)
    hashed_token_info = hashed_token_proof(token_proof=token_info)
    query_token_data(blockchain_address=blockchain_address,
                     hashed_proofs=[hashed_token_info],
                     token_symbols=[token_symbol])


def query_default_token(chain_str: str):
    """This function synchronously queries cic-eth for the deployed system's default token.
    :param chain_str: Chain name and network id.
    :type chain_str: str
    :return: Token's data.
    :rtype: dict
    """
    logg.debug(f'Querying API for default token on chain: {chain_str}')
    cic_eth_api = Api(chain_str=chain_str)
    default_token_request_task = cic_eth_api.default_token()
    return default_token_request_task.get()


def query_token_data(blockchain_address: str, hashed_proofs: list, token_symbols: list):
    """"""
    logg.debug(f'Retrieving token metadata for tokens: {", ".join(token_symbols)}')
    api = Api(callback_param=blockchain_address,
              callback_queue='cic-ussd',
              chain_str=Chain.spec.__str__(),
              callback_task='cic_ussd.tasks.callback_handler.token_data_callback')
    api.tokens(token_symbols=token_symbols, proof=hashed_proofs)


def remove_from_account_tokens_list(account_tokens_list: list, token_symbol: str):
    """
    :param account_tokens_list:
    :type account_tokens_list:
    :param token_symbol:
    :type token_symbol:
    :return:
    :rtype:
    """
    removed_token_data = []
    for i in range(len(account_tokens_list)):
        if account_tokens_list[i]['symbol'] == token_symbol:
            removed_token_data.append(account_tokens_list[i])
            del account_tokens_list[i]
            break
    return removed_token_data, account_tokens_list


def set_active_token(blockchain_address: str, token_symbol: str):
    """
    :param blockchain_address:
    :type blockchain_address:
    :param token_symbol:
    :type token_symbol:
    :return:
    :rtype:
    """
    logg.info(f'Active token set to: {token_symbol}')
    key = cache_data_key(identifier=bytes.fromhex(blockchain_address), salt=MetadataPointer.TOKEN_ACTIVE)
    cache_data(key=key, data=token_symbol)


def token_list_set(preferred_language: str, token_data_reprs: list):
    """
    :param preferred_language:
    :type preferred_language:
    :param token_data_reprs:
    :type token_data_reprs:
    :return:
    :rtype:
    """
    if not token_data_reprs:
        return translation_for('helpers.no_tokens_list', preferred_language)
    return ''.join(f'{token_data_repr}\n' for token_data_repr in token_data_reprs)

