from .rpc.request import (
    rpc_request
)

from .rpc.exceptions import (
    RPCError,
    RequestsError,
    RequestsTimeoutError
)

from .exceptions import (
    InvalidRPCReplyError
)

from .blockchain import (
    get_sharding_structure
)

from .bech32.bech32 import (
    bech32_decode
)

_default_endpoint = 'http://localhost:9500'
_default_timeout = 30
_address_length = 42


def is_valid_address(address) -> bool:
    """
    Check if given string is valid one address
    NOTE: This function is NOT thread safe due to the C function used by the bech32 library.

    Parameters
    ----------
    address: str
        String to check if valid one address

    Returns
    -------
    bool
        Is valid address
    """
    if not address.startswith('one1'):
        return False
    hrp, _ = bech32_decode(address)
    if not hrp:
        return False
    return True

def get_balance(address, endpoint=_default_endpoint, timeout=_default_timeout) -> int:
    """
    Get current account balance

    Parameters
    ----------
    address: str
        Address to get balance for
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    int
        Account balance in ATTO

    Raises
    ------
    InvalidRPCReplyError
        If received unknown result from endpoint

    API Reference
    -------------
    https://api.hmny.io/#da8901d2-d237-4c3b-9d7d-10af9def05c4
    """
    method = 'hmyv2_getBalance'
    params = [
        address
    ]
    try:
        balance = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
        return int(balance)     # v2 returns the result as it is
    except TypeError as e:      # check will work if rpc returns None
        raise InvalidRPCReplyError(method, endpoint) from e

def get_balance_by_block(address, block_num, endpoint=_default_endpoint, timeout=_default_timeout) -> int:
    """
    Get account balance for address at a given block number

    Parameters
    ----------
    address: str
        Address to get balance for
    block_num: int
        Block to get balance at
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    int
        Account balance in ATTO

    Raises
    ------
    InvalidRPCReplyError
        If received unknown result from endpoint

    API Reference
    -------------
    https://api.hmny.io/#9aeae4b8-1a09-4ed2-956b-d7c96266dd33
    https://github.com/harmony-one/harmony/blob/9f320436ff30d9babd957bc5f2e15a1818c86584/rpc/blockchain.go#L92
    """
    method = 'hmyv2_getBalanceByBlockNumber'
    params = [
        address,
        block_num
    ]
    try:
        balance = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
        return int(balance)
    except TypeError as e:
        raise InvalidRPCReplyError(method, endpoint) from e

def get_account_nonce(address, block_num, endpoint=_default_endpoint, timeout=_default_timeout) -> int:
    """
    Get the account nonce

    Parameters
    ----------
    address: str
        Address to get transaction count for
    block_num: :obj:`int` or 'latest'
        Block to get nonce at
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    int
        Account nonce

    Raises
    ------
    InvalidRPCReplyError
        If received unknown result from endpoint

    API Reference
    -------------
    https://github.com/harmony-one/harmony/blob/9f320436ff30d9babd957bc5f2e15a1818c86584/rpc/transaction.go#L51
    """
    method = 'hmyv2_getAccountNonce'
    params = [
        address,
        block_num
    ]
    try:
        nonce = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
        return int(nonce)
    except TypeError as e:
        raise InvalidRPCReplyError(method, endpoint) from e

def get_transaction_count(address, block_num, endpoint=_default_endpoint, timeout=_default_timeout) -> int:
    """
    Get the number of transactions the given address has sent for the given block number
    Legacy for apiv1. For apiv2, please use get_account_nonce/get_transactions_count/get_staking_transactions_count apis for
    more granular transaction counts queries

    Parameters
    ----------
    address: str
        Address to get transaction count for
    block_num: :obj:`int` or 'latest'
        Block to get nonce at
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    int
        The number of transactions the given address has sent for the given block number

    Raises
    ------
    InvalidRPCReplyError
        If received unknown result from endpoint

    API Reference
    -------------
    https://github.com/harmony-one/harmony/blob/9f320436ff30d9babd957bc5f2e15a1818c86584/rpc/transaction.go#L69
    """
    method = 'hmyv2_getTransactionCount'
    params = [
        address,
        block_num
    ]
    try:
        nonce = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
        return int(nonce)
    except TypeError as e:
        raise InvalidRPCReplyError(method, endpoint) from e

def get_transactions_count(address, tx_type, endpoint=_default_endpoint, timeout=_default_timeout) -> int:
    """
    Get the number of regular transactions from genesis of input type

    Parameters
    ----------
    address: str
        Address to get transaction count for
    tx_type: str
        Type of transactions to include in the count
        currently supported are 'SENT', 'RECEIVED', 'ALL'
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    int
        Count of transactions of type tx_type

    Raises
    ------
    InvalidRPCReplyError
        If received unknown result from endpoint

    API Reference
    -------------
    https://api.hmny.io/#fc97aed2-e65e-4cf4-bc01-8dadb76732c0
    https://github.com/harmony-one/harmony/blob/9f320436ff30d9babd957bc5f2e15a1818c86584/rpc/transaction.go#L114
    """
    method = 'hmyv2_getTransactionsCount'
    params = [
        address,
        tx_type
    ]
    try:
        tx_count = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
        return int(tx_count)
    except TypeError as e:
        raise InvalidRPCReplyError(method, endpoint) from e

def get_staking_transactions_count(address, tx_type, endpoint=_default_endpoint, timeout=_default_timeout) -> int:
    """
    Get the number of staking transactions from genesis of input type ("SENT", "RECEIVED", "ALL")

    Parameters
    ----------
    address: str
        Address to get staking transaction count for
    tx_type: str
        Type of staking transactions to include in the count
        currently supported are 'SENT', 'RECEIVED', 'ALL'
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    int
        Count of staking transactions of type tx_type

    Raises
    ------
    InvalidRPCReplyError
        If received unknown result from endpoint

    API Reference
    -------------
    https://api.hmny.io/#ddc1b029-f341-4c4d-ba19-74b528d6e5e5
    https://github.com/harmony-one/harmony/blob/9f320436ff30d9babd957bc5f2e15a1818c86584/rpc/transaction.go#L134
    """
    method = 'hmyv2_getStakingTransactionsCount'
    params = [
        address,
        tx_type
    ]
    try:
        tx_count = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
        return int(tx_count)
    except (KeyError, TypeError) as e:
        raise InvalidRPCReplyError(method, endpoint) from e

def get_transaction_history(address, page=0, page_size=1000, include_full_tx=False, tx_type='ALL',
        order='ASC', endpoint=_default_endpoint, timeout=_default_timeout
    ) -> list:
    """
    Get list of transactions sent and/or received by the account

    Parameters
    ----------
    address: str
        Address to get transaction history for
    page: :obj:`int`, optional
        Page to request for pagination
    page_size: :obj:`int`, optional
        Size of page for pagination
    include_full_tx: :obj:`bool`, optional
        True to include full transaction data
        False to just get the transaction hash
    tx_type: :obj:`str`, optional
        'ALL' to get all transactions send & received by the address
        'SENT' to get all transactions sent by the address
        'RECEIVED' to get all transactions received by the address
    order: :obj:`str`, optional
        'ASC' to sort transactions in ascending order based on timestamp (oldest first)
        'DESC' to sort transactions in descending order based on timestamp (newest first)
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    list of transactions
    if include_full_tx is True, each transaction is a dictionary with the following keys
        see transaction/get_transaction_by_hash for a description
    if include_full_tx is False, each element represents the transaction hash

    Raises
    ------
    InvalidRPCReplyError
        If received unknown result from endpoint

    API Reference
    -------------
    https://api.hmny.io/#2200a088-81b5-4420-a291-312a7c6d880e
    https://github.com/harmony-one/harmony/blob/9f320436ff30d9babd957bc5f2e15a1818c86584/rpc/transaction.go#L255
    """
    params = [
        {
            'address': address,
            'pageIndex': page,
            'pageSize': page_size,
            'fullTx': include_full_tx,
            'txType': tx_type,
            'order': order
        }
    ]
    method = 'hmyv2_getTransactionsHistory'
    try:
        tx_history = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)
        return tx_history['result']['transactions']
    except KeyError as e:
        raise InvalidRPCReplyError(method, endpoint) from e

def get_staking_transaction_history(address, page=0, page_size=1000, include_full_tx=False, tx_type='ALL',
        order='ASC', endpoint=_default_endpoint, timeout=_default_timeout
    ) -> list:
    """
    Get list of staking transactions sent by the account

    Parameters
    ----------
    address: str
        Address to get staking transaction history for
    page: :obj:`int`, optional
        Page to request for pagination
    page-size: :obj:`int`, optional
        Size of page for pagination
    include_full_tx: :obj:`bool`, optional
        True to include full staking transaction data
        False to just get the staking transaction hash
    tx_type: :obj:`str`, optional
        'ALL' to get all staking transactions
    order: :obj:`str`, optional
        'ASC' to sort transactions in ascending order based on timestamp
        'DESC' to sort transactions in descending order based on timestamp
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    list of transactions
    if include_full_tx is True, each transaction is a dictionary with the following kets
        blockHash: :obj:`str` Block hash that transaction was finalized; "0x0000000000000000000000000000000000000000000000000000000000000000" if tx is pending
        blockNumber: :obj:`int` Block number that transaction was finalized; None if tx is pending
        from: :obj:`str` Wallet address
        timestamp: :obj:`int` Timestamp in Unix time when transaction was finalized
        gas: :obj:`int` Gas limit in Atto
        gasPrice :obj:`int` Gas price in Atto
        hash: :obj:`str` Transaction hash
        nonce: :obj:`int` Wallet nonce for the transaction
        transactionIndex: :obj:`int` Index of transaction in block; None if tx is pending
        type: :obj:`str` Type of staking transaction, for example, "CollectRewards", "Delegate", "Undelegate"
        msg: :obj:`dict` Message attached to the staking transaction
        r: :obj:`str` First 32 bytes of the transaction signature
        s: :obj:`str` Next  32 bytes of the transaction signature
        v: :obj:`str` Recovery value + 27, as hex string
    if include_full_tx is False, each element represents the transaction hash

    Raises
    ------
    InvalidRPCReplyError
        If received unknown result from endpoint

    API Reference
    -------------
    https://api.hmny.io/#c5d25b36-57be-4e43-a23b-17ace350e322
    https://github.com/harmony-one/harmony/blob/9f320436ff30d9babd957bc5f2e15a1818c86584/rpc/transaction.go#L303
    """
    params = [
        {
            'address': address,
            'pageIndex': page,
            'pageSize': page_size,
            'fullTx': include_full_tx,
            'txType': tx_type,
            'order': order
        }
    ]
    # Using v2 API, because getStakingTransactionHistory not implemented in v1
    method = 'hmyv2_getStakingTransactionsHistory'
    try:
        stx_history =  rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
        return stx_history['staking_transactions']
    except KeyError as e:
        raise InvalidRPCReplyError(method, endpoint) from e

def get_balance_on_all_shards(address, skip_error=True, endpoint=_default_endpoint, timeout=_default_timeout) -> list:
    """
    Get current account balance in all shards & optionally report errors getting account balance for a shard

    Parameters
    ----------
    address: str
        Address to get balance for
    skip_error: :obj:`bool`, optional
        True to ignore errors getting balance for shard
        False to include errors when getting balance for shard
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    timeout: :obj:`int`, optional
        Timeout in seconds per request

    Returns
    -------
    list of dictionaries, each dictionary to contain shard number and balance of that shard in ATTO
        Example reply:
        [
            {
                'shard': 0,
                'balance': 0,
            },
            ...
        ]
    """
    balances = []
    sharding_structure = get_sharding_structure(endpoint=endpoint, timeout=timeout)
    for shard in sharding_structure:
        try:
            balances.append({
                'shard': shard['shardID'],
                'balance': get_balance(address, endpoint=shard['http'], timeout=timeout)
            })
        except (KeyError, RPCError, RequestsError, RequestsTimeoutError):
            if not skip_error:
                balances.append({
                    'shard': shard['shardID'],
                    'balance': None
                })
    return balances

def get_total_balance(address, endpoint=_default_endpoint, timeout=_default_timeout) -> int:
    """
    Get total account balance on all shards

    Parameters
    ----------
    address: str
        Address to get balance for
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    timeout: :obj:`int`, optional
        Timeout in seconds per request

    Returns
    -------
    int
        Total account balance in ATTO

    Raises
    ------
    RuntimeError
        If error occurred getting account balance for a shard

    See also
    ------
    get_balance_on_all_shards
    """
    try:
        balances = get_balance_on_all_shards(address, skip_error=False, endpoint=endpoint, timeout=timeout)
        return sum(b['balance'] for b in balances)
    except TypeError as e:
        raise RuntimeError from e
