from .rpc.request import (
    rpc_request
)

from .exceptions import (
    InvalidRPCReplyError
)

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


################
# Network RPCs #
################
def get_node_metadata(endpoint=_default_endpoint, timeout=_default_timeout) -> dict:
    """
    Get config for the node

    Parameters
    ----------
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    dict
        # TODO: Add link to reference RPC documentation
    """
    return rpc_request('hmy_getNodeMetadata', endpoint=endpoint, timeout=timeout)['result']


def get_shard(endpoint=_default_endpoint, timeout=_default_timeout) -> int:
    """
    Get config for the node

    Parameters
    ----------
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    int
        Shard ID of node

    Raises
    ------
    InvalidRPCReplyError
        If received unknown result from endpoint
    """
    method = 'hmy_getNodeMetadata'
    try:
        return rpc_request(method, endpoint=endpoint, timeout=timeout)['result']['shard-id']
    except KeyError as e:
        raise InvalidRPCReplyError(method, endpoint) from e


def get_staking_epoch(endpoint=_default_endpoint, timeout=_default_timeout) -> int:
    """
    Get epoch number when blockchain switches to EPoS election

    Parameters
    ----------
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    int
        Epoch at which blockchain switches to EPoS election

    Raises
    ------
    InvalidRPCReplyError
        If received unknown result from endpoint
    """
    method = 'hmy_getNodeMetadata'
    data = rpc_request(method, endpoint=endpoint, timeout=timeout)['result']
    try:
        return int(data['chain-config']['staking-epoch'])
    except (KeyError, TypeError) as e:
        raise InvalidRPCReplyError(method, endpoint) from e


def get_prestaking_epoch(endpoint=_default_endpoint, timeout=_default_timeout) -> int:
    """
    Get epoch number when blockchain switches to allow staking features without election

    Parameters
    ----------
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    int
        Epoch at which blockchain switches to allow staking features without election

    Raises
    ------
    InvalidRPCReplyError
        If received unknown result from endpoint
    """
    method = 'hmy_getNodeMetadata'
    data = rpc_request(method, endpoint=endpoint, timeout=timeout)['result']
    try:
        return int(data['chain-config']['prestaking-epoch'])
    except (KeyError, TypeError) as e:
        raise InvalidRPCReplyError(method, endpoint) from e


def get_sharding_structure(endpoint=_default_endpoint, timeout=_default_timeout) -> list:
    """
    Get network sharding structure

    Parameters
    ----------
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    list
        # TODO: Add link to reference RPC documentation
    """
    return rpc_request('hmy_getShardingStructure', endpoint=endpoint, timeout=timeout)['result']


def get_leader_address(endpoint=_default_endpoint, timeout=_default_timeout) -> str:
    """
    Get current leader one address

    Parameters
    ----------
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    str
        One address of current leader
    """
    return rpc_request('hmy_getLeader', endpoint=endpoint, timeout=timeout)['result']


def get_block_number(endpoint=_default_endpoint, timeout=_default_timeout) -> int:
    """
    Get current block number

    Parameters
    ----------
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    int
        Current block number

    Raises
    ------
    InvalidRPCReplyError
        If received unknown result from endpoint
    """
    method = 'hmy_blockNumber'
    try:
        return int(rpc_request(method, endpoint=endpoint, timeout=timeout)['result'], 16)
    except TypeError as e:
        raise InvalidRPCReplyError(method, endpoint) from e


def get_current_epoch(endpoint=_default_endpoint, timeout=_default_timeout) -> int:
    """
    Get current epoch number

    Parameters
    ----------
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    int
        Current epoch number

    Raises
    ------
    InvalidRPCReplyError
        If received unknown result from endpoint
    """
    method = 'hmy_getEpoch'
    try:
        return int(rpc_request(method, endpoint=endpoint, timeout=timeout)['result'], 16)
    except TypeError as e:
        raise InvalidRPCReplyError(method, endpoint) from e


def get_gas_price(endpoint=_default_endpoint, timeout=_default_timeout) -> int:
    """
    Get network gas price

    Parameters
    ----------
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    int
        Network gas price

    Raises
    ------
    InvalidRPCReplyError
        If received unknown result from endpoint
    """
    method = 'hmy_gasPrice'
    try:
        return int(rpc_request(method, endpoint=endpoint, timeout=timeout)['result'], 16)
    except TypeError as e:
        raise InvalidRPCReplyError(method, endpoint) from e


def get_num_peers(endpoint=_default_endpoint, timeout=_default_timeout) -> int:
    """
    Get number of peers connected to the node

    Parameters
    ----------
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    int
        Number of connected peers
    Raises
    ------
    InvalidRPCReplyError
        If received unknown result from endpoint
    """
    method = 'net_peerCount'
    try:
        return int(rpc_request(method, endpoint=endpoint, timeout=timeout)['result'], 16)
    except TypeError as e:
        raise InvalidRPCReplyError(method, endpoint) from e


##############
# Block RPCs #
##############
def get_latest_header(endpoint=_default_endpoint, timeout=_default_timeout) -> dict:
    """
    Get block header of latest block

    Parameters
    ----------
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    dict
        # TODO: Add link to reference RPC documentation
    """
    return rpc_request('hmy_latestHeader', endpoint=endpoint, timeout=timeout)['result']


def get_latest_headers(endpoint=_default_endpoint, timeout=_default_timeout) -> dict:
    """
    Get block header of latest block for beacon chain & shard chain

    Parameters
    ----------
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    dict
        # TODO: Add link to reference RPC documentation
    """
    return rpc_request('hmy_getLatestChainHeaders', endpoint=endpoint, timeout=timeout)['result']


def get_block_by_number(block_num, endpoint=_default_endpoint, include_full_tx=False, timeout=_default_timeout) -> dict:
    """
    Get block by number

    Parameters
    ----------
    block_num: int
        Block number to fetch
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    include_full_tx: :obj:`bool`, optional
        Include list of full transactions data for each block
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    dict
        # TODO: Add link to reference RPC documentation
    """
    params = [
        str(hex(block_num)),
        include_full_tx
    ]
    return rpc_request('hmy_getBlockByNumber', params=params, endpoint=endpoint, timeout=timeout)['result']


def get_block_by_hash(block_hash, endpoint=_default_endpoint, include_full_tx=False, timeout=_default_timeout) -> dict:
    """
    Get block by hash

    Parameters
    ----------
    block_hash: str
        Block hash to fetch
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    include_full_tx: :obj:`bool`, optional
        Include list of full transactions data for each block
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    dict
        # TODO: Add link to reference RPC documentation
        None if block hash is not found
    """
    params = [
        block_hash,
        include_full_tx
    ]
    return rpc_request('hmy_getBlockByHash', params=params, endpoint=endpoint, timeout=timeout)['result']


def get_block_transaction_count_by_number(block_num, endpoint=_default_endpoint, timeout=_default_timeout) -> int:
    """
    Get transaction count for specific block number

    Parameters
    ----------
    block_num: int
        Block number to get transaction count for
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    include_full_tx: :obj:`bool`, optional
        Include list of full transactions data for each block
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    int
        Number of transactions in the block
    """
    params = [
        str(hex(block_num))
    ]
    return int(rpc_request('hmy_getBlockTransactionCountByNumber', params=params,
        endpoint=endpoint, timeout=timeout)['result'], 16
    )


def get_block_transaction_count_by_hash(block_hash, endpoint=_default_endpoint, timeout=_default_timeout) -> int:
    """
    Get transaction count for specific block hash for

    Parameters
    ----------
    block_hash: str
        Block hash to get transaction count
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    include_full_tx: :obj:`bool`, optional
        Include list of full transactions data for each block
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    int
        Number of transactions in the block
    """
    params = [
        block_hash
    ]
    return int(rpc_request('hmy_getBlockTransactionCountByHash', params=params,
        endpoint=endpoint, timeout=timeout)['result'], 16
    )


def get_blocks(start_block, end_block, endpoint=_default_endpoint, include_full_tx=False,
        include_signers=False, timeout=_default_timeout
    ) -> list:
    """
    Get list of blocks from a range

    Parameters
    ----------
    start_block: int
        First block to fetch (inclusive)
    end_block: int
        Last block to fetch (inclusive)
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    include_full_tx: :obj:`bool`, optional
        Include list of full transactions data for each block
    include_signers: :obj:`bool`, optional
        Include list of signers for each block
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    list
        # TODO: Add link to reference RPC documentation
    """
    params = [
        str(hex(start_block)),
        str(hex(end_block)),
        {
            'withSigners': include_signers,
            'fullTx': include_full_tx,
        },
    ]
    return rpc_request('hmy_getBlocks', params=params, endpoint=endpoint, timeout=timeout)['result']


def get_block_signers(block_num, endpoint=_default_endpoint, timeout=_default_timeout) -> list:
    """
    Get list of block signers for specific block number

    Parameters
    ----------
    block_num: int
        Block number to get signers for
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    list
        List of one addresses that signed the block
    """
    params = [
        str(hex(block_num))
    ]
    return rpc_request('hmy_getBlockSigners', params=params, endpoint=endpoint, timeout=timeout)['result']


def get_block_signer_keys(block_num, endpoint=_default_endpoint, timeout=_default_timeout) -> list:
    """
    Get list of block signer public bls keys for specific block number

    Parameters
    ----------
    block_num: int
        Block number to get signer keys for
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    list
        List of bls public keys that signed the block
    """
    params = [
        str(hex(block_num))
    ]
    return rpc_request('hmy_getBlockSignerKeys', params=params, endpoint=endpoint, timeout=timeout)['result']


def get_validators(epoch, endpoint=_default_endpoint, timeout=_default_timeout) -> dict:
    """
    Get list of validators for specific epoch number

    Parameters
    ----------
    epoch: int
        Epoch to get list of validators for
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    dict
        # TODO: Add link to reference RPC documentation
    """
    params = [
        epoch
    ]
    return rpc_request('hmy_getValidators', params=params, endpoint=endpoint, timeout=timeout)['result']


def get_validator_keys(epoch, endpoint=_default_endpoint, timeout=_default_timeout) -> list:
    """
    Get list of validator public bls keys for specific epoch number

    Parameters
    ----------
    epoch: int
        Epoch to get list of validator keys for
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    list
        List of bls public keys in the validator committee
    """
    params = [
        epoch
    ]
    return rpc_request('hmy_getValidatorKeys', params=params, endpoint=endpoint, timeout=timeout)['result']


def get_bad_blocks(endpoint=_default_endpoint, timeout=_default_timeout) -> list:
    """
    Get list of bad blocks in memory of specific node

    Parameters
    ----------
    endpoint: :obj:`str`, optional
        Endpoint to send request to
    timeout: :obj:`int`, optional
        Timeout in seconds

    Returns
    -------
    list
        # TODO: Add link to reference RPC documentation
    """
    return rpc_request('hmy_getCurrentBadBlocks', endpoint=endpoint, timeout=timeout)['result']
