from abc import (
    ABC,
    abstractmethod,
)
import logging
from typing import (
    TYPE_CHECKING,
    Tuple,
    Type,
)

from eth._utils.datatypes import (
    Configurable,
)
from eth.db.backends.base import (
    BaseAtomicDB,
)
from eth.exceptions import (
    BlockNotFound,
)
from eth.validation import (
    validate_word,
)
from eth_typing import (
    Hash32,
)
from eth_utils import (
    ValidationError,
    encode_hex,
)

from eth2._utils.ssz import (
    validate_imported_block_unchanged,
)
from eth2.beacon.db.chain import (
    BaseBeaconChainDB,
    BeaconChainDB,
)
from eth2.beacon.exceptions import (
    BlockClassError,
    StateMachineNotFound,
)
from eth2.beacon.types.blocks import (
    BaseBeaconBlock,
)
from eth2.beacon.types.states import (
    BeaconState,
)
from eth2.beacon.typing import (
    FromBlockParams,
    Slot,
)
from eth2.beacon.validation import (
    validate_slot,
)

if TYPE_CHECKING:
    from eth2.beacon.state_machines.base import (  # noqa: F401
        BaseBeaconStateMachine,
    )


class BaseBeaconChain(Configurable, ABC):
    """
    The base class for all BeaconChain objects
    """
    chaindb = None  # type: BaseBeaconChainDB
    chaindb_class = None  # type: Type[BaseBeaconChainDB]
    sm_configuration = None  # type: Tuple[Tuple[Slot, Type[BaseBeaconStateMachine]], ...]
    chain_id = None  # type: int

    #
    # Helpers
    #
    @classmethod
    @abstractmethod
    def get_chaindb_class(cls) -> Type[BaseBeaconChainDB]:
        pass

    #
    # Chain API
    #
    @classmethod
    @abstractmethod
    def from_genesis(cls,
                     base_db: BaseAtomicDB,
                     genesis_state: BeaconState,
                     genesis_block: BaseBeaconBlock) -> 'BaseBeaconChain':
        pass

    #
    # State Machine API
    #
    @classmethod
    @abstractmethod
    def get_state_machine_class(
            cls,
            block: BaseBeaconBlock) -> Type['BaseBeaconStateMachine']:
        pass

    @abstractmethod
    def get_state_machine(self, at_block: BaseBeaconBlock=None) -> 'BaseBeaconStateMachine':
        pass

    @classmethod
    @abstractmethod
    def get_state_machine_class_for_block_slot(
            cls,
            slot: Slot) -> Type['BaseBeaconStateMachine']:
        pass

    #
    # Block API
    #
    @abstractmethod
    def get_block_class(self, block_root: Hash32) -> Type[BaseBeaconBlock]:
        pass

    @abstractmethod
    def create_block_from_parent(self,
                                 parent_block: BaseBeaconBlock,
                                 block_params: FromBlockParams) -> BaseBeaconBlock:
        pass

    @abstractmethod
    def get_block_by_root(self, block_root: Hash32) -> BaseBeaconBlock:
        pass

    @abstractmethod
    def get_canonical_head(self) -> BaseBeaconBlock:
        pass

    @abstractmethod
    def get_score(self, block_root: Hash32) -> int:
        pass

    @abstractmethod
    def ensure_block(self, block: BaseBeaconBlock=None) -> BaseBeaconBlock:
        pass

    @abstractmethod
    def get_block(self) -> BaseBeaconBlock:
        pass

    @abstractmethod
    def get_canonical_block_by_slot(self, slot: Slot) -> BaseBeaconBlock:
        pass

    @abstractmethod
    def get_canonical_block_root(self, slot: Slot) -> Hash32:
        pass

    @abstractmethod
    def import_block(
            self,
            block: BaseBeaconBlock,
            perform_validation: bool=True
    ) -> Tuple[BaseBeaconBlock, Tuple[BaseBeaconBlock, ...], Tuple[BaseBeaconBlock, ...]]:
        pass


class BeaconChain(BaseBeaconChain):
    """
    A Chain is a combination of one or more ``StateMachine`` classes.  Each ``StateMachine``
    is associated with a range of slots. The Chain class acts as a wrapper around these other
    StateMachine classes, delegating operations to the appropriate StateMachine depending on the
    current block slot number.
    """
    logger = logging.getLogger("eth2.beacon.chains.BeaconChain")

    chaindb_class = BeaconChainDB  # type: Type[BaseBeaconChainDB]

    def __init__(self, base_db: BaseAtomicDB) -> None:
        if not self.sm_configuration:
            raise ValueError(
                "The Chain class cannot be instantiated with an empty `sm_configuration`"
            )
        else:
            # TODO implment validate_sm_configuration(self.sm_configuration)
            # validate_sm_configuration(self.sm_configuration)
            pass

        self.chaindb = self.get_chaindb_class()(base_db)

    #
    # Helpers
    #
    @classmethod
    def get_chaindb_class(cls) -> Type['BaseBeaconChainDB']:
        if cls.chaindb_class is None:
            raise AttributeError("`chaindb_class` not set")
        return cls.chaindb_class

    #
    # Chain API
    #
    @classmethod
    def from_genesis(cls,
                     base_db: BaseAtomicDB,
                     genesis_state: BeaconState,
                     genesis_block: BaseBeaconBlock) -> 'BaseBeaconChain':
        """
        Initialize the ``BeaconChain`` from a genesis state.
        """
        sm_class = cls.get_state_machine_class_for_block_slot(genesis_block.slot)
        if type(genesis_block) != sm_class.block_class:
            raise BlockClassError(
                "Given genesis block class: {}, StateMachine.block_class: {}".format(
                    type(genesis_block),
                    sm_class.block_class
                )
            )

        chaindb = cls.get_chaindb_class()(db=base_db)
        chaindb.persist_state(genesis_state)
        return cls._from_genesis_block(base_db, genesis_block)

    @classmethod
    def _from_genesis_block(cls,
                            base_db: BaseAtomicDB,
                            genesis_block: BaseBeaconBlock) -> 'BaseBeaconChain':
        """
        Initialize the ``BeaconChain`` from the genesis block.
        """
        chaindb = cls.get_chaindb_class()(db=base_db)
        chaindb.persist_block(genesis_block, genesis_block.__class__)
        return cls(base_db)

    #
    # StateMachine API
    #
    @classmethod
    def get_state_machine_class(cls, block: BaseBeaconBlock) -> Type['BaseBeaconStateMachine']:
        """
        Returns the ``StateMachine`` instance for the given block slot number.
        """
        return cls.get_state_machine_class_for_block_slot(block.slot)

    @classmethod
    def get_state_machine_class_for_block_slot(
            cls,
            slot: Slot) -> Type['BaseBeaconStateMachine']:
        """
        Return the ``StateMachine`` class for the given block slot number.
        """
        if cls.sm_configuration is None:
            raise AttributeError("Chain classes must define the StateMachines in sm_configuration")

        validate_slot(slot)
        for start_slot, sm_class in reversed(cls.sm_configuration):
            if slot >= start_slot:
                return sm_class
        raise StateMachineNotFound("No StateMachine available for block slot: #{0}".format(slot))

    def get_state_machine(self, at_block: BaseBeaconBlock=None) -> 'BaseBeaconStateMachine':
        """
        Return the ``StateMachine`` instance for the given block number.
        """
        block = self.ensure_block(at_block)
        sm_class = self.get_state_machine_class_for_block_slot(block.slot)

        return sm_class(
            chaindb=self.chaindb,
            block=block,
        )

    #
    # Block API
    #
    def get_block_class(self, block_root: Hash32) -> Type[BaseBeaconBlock]:
        slot = self.chaindb.get_slot_by_root(block_root)
        sm_class = self.get_state_machine_class_for_block_slot(slot)
        block_class = sm_class.block_class
        return block_class

    def create_block_from_parent(self,
                                 parent_block: BaseBeaconBlock,
                                 block_params: FromBlockParams) -> BaseBeaconBlock:
        """
        Passthrough helper to the ``StateMachine`` class of the block descending from the
        given block.
        """
        return self.get_state_machine_class_for_block_slot(
            slot=parent_block.slot + 1 if block_params.slot is None else block_params.slot,
        ).create_block_from_parent(parent_block, block_params)

    def get_block_by_root(self, block_root: Hash32) -> BaseBeaconBlock:
        """
        Return the requested block as specified by block hash.

        Raise ``BlockNotFound`` if there's no block with the given hash in the db.
        """
        validate_word(block_root, title="Block Hash")

        block_class = self.get_block_class(block_root)
        return self.chaindb.get_block_by_root(block_root, block_class)

    def get_canonical_head(self) -> BaseBeaconBlock:
        """
        Return the block at the canonical chain head.

        Raise ``CanonicalHeadNotFound`` if there's no head defined for the canonical chain.
        """
        block_root = self.chaindb.get_canonical_head_root()

        block_class = self.get_block_class(block_root)
        return self.chaindb.get_block_by_root(block_root, block_class)

    def get_score(self, block_root: Hash32) -> int:
        """
        Return the score of the block with the given hash.

        Raise ``BlockNotFound`` if there is no matching black hash.
        """
        return self.chaindb.get_score(block_root)

    def ensure_block(self, block: BaseBeaconBlock=None) -> BaseBeaconBlock:
        """
        Return ``block`` if it is not ``None``, otherwise return the block
        of the canonical head.
        """
        if block is None:
            head = self.get_canonical_head()
            return self.create_block_from_parent(head, FromBlockParams())
        else:
            return block

    def get_block(self) -> BaseBeaconBlock:
        """
        Return the current TIP block.
        """
        return self.get_state_machine().block

    def get_canonical_block_by_slot(self, slot: Slot) -> BaseBeaconBlock:
        """
        Return the block with the given number in the canonical chain.

        Raise ``BlockNotFound`` if there's no block with the given number in the
        canonical chain.
        """
        validate_slot(slot)
        return self.get_block_by_root(self.chaindb.get_canonical_block_root(slot))

    def get_canonical_block_root(self, slot: Slot) -> Hash32:
        """
        Return the block hash with the given number in the canonical chain.

        Raise ``BlockNotFound`` if there's no block with the given number in the
        canonical chain.
        """
        return self.chaindb.get_canonical_block_root(slot)

    def import_block(
            self,
            block: BaseBeaconBlock,
            perform_validation: bool=True
    ) -> Tuple[BaseBeaconBlock, Tuple[BaseBeaconBlock, ...], Tuple[BaseBeaconBlock, ...]]:
        """
        Import a complete block and returns a 3-tuple

        - the imported block
        - a tuple of blocks which are now part of the canonical chain.
        - a tuple of blocks which were canonical and now are no longer canonical.
        """

        try:
            parent_block = self.get_block_by_root(block.previous_block_root)
        except BlockNotFound:
            raise ValidationError(
                "Attempt to import block #{}.  Cannot import block {} before importing "
                "its parent block at {}".format(
                    block.slot,
                    block.signed_root,
                    block.previous_block_root,
                )
            )
        base_block_for_import = self.create_block_from_parent(
            parent_block,
            FromBlockParams(),
        )
        state, imported_block = self.get_state_machine(base_block_for_import).import_block(block)

        # Validate the imported block.
        if perform_validation:
            validate_imported_block_unchanged(imported_block, block)

        # TODO: Now it just persists all state. Should design how to clean up the old state.
        self.chaindb.persist_state(state)

        (
            new_canonical_blocks,
            old_canonical_blocks,
        ) = self.chaindb.persist_block(imported_block, imported_block.__class__)

        self.logger.debug(
            'IMPORTED_BLOCK: slot %s | signed root %s',
            imported_block.slot,
            encode_hex(imported_block.signed_root),
        )

        return imported_block, new_canonical_blocks, old_canonical_blocks

[(169, 'logging.getLogger', 'logging.getLogger', False, 'import logging\n'), (249, 'eth2.beacon.validation.validate_slot', 'validate_slot', False, 'from eth2.beacon.validation import validate_slot\n'), (293, 'eth.validation.validate_word', 'validate_word', False, 'from eth.validation import validate_word\n'), (341, 'eth2.beacon.validation.validate_slot', 'validate_slot', False, 'from eth2.beacon.validation import validate_slot\n'), (379, 'eth2.beacon.typing.FromBlockParams', 'FromBlockParams', False, 'from eth2.beacon.typing import FromBlockParams, Slot\n'), (385, 'eth2._utils.ssz.validate_imported_block_unchanged', 'validate_imported_block_unchanged', False, 'from eth2._utils.ssz import validate_imported_block_unchanged\n'), (398, 'eth_utils.encode_hex', 'encode_hex', False, 'from eth_utils import ValidationError, encode_hex\n'), (324, 'eth2.beacon.typing.FromBlockParams', 'FromBlockParams', False, 'from eth2.beacon.typing import FromBlockParams, Slot\n')]