"""Every persistence service is implementing one of interface defined in this
module. Interfaces are generics depends on at least Entity class (finders needs
also request class).

:Example: Implementing of persistence service

>>> from acd.domain import Entity
...
>>> class Example(Entity, id_attributes=('num',)):
...     num: int
...
>>> class ExampleInserter(Inserter[Example]):
...     def __init__(self, dbsession):
...         self._session = dbsession
...
...     def __call__(self, entity: Example) -> Example:
...         self._session.add(entity)
...         return entity
"""
from abc import ABCMeta, abstractmethod
from typing import TypeVar, Generic, Optional, List

from acd.domain import EntityId


class PersistenceError(IOError):
    """Root exception for persistence layer exceptions.
    """


class EntityDuplicateError(PersistenceError):
    """Raises when inserting duplicate of entity.
    """


class EntityNotFoundError(PersistenceError):
    """Raises when trying to update not existing entity.
    """


Entity = TypeVar('Entity')
SearchRequest = TypeVar('SearchRequest')


class Inserter(Generic[Entity], metaclass=ABCMeta):
    """Stores entity.

    :Example: Inserter implementation

    >>> from acd.domain import Entity
    >>> class Example(Entity, id_attributes=('num',)):
    ...    num: int
    ...
    >>> class ExampleInserter(Inserter[Example]):
    ...     def __init__(self, cache: dict) -> None:
    ...         self._cache = cache
    ...
    ...     def __call__(self, entity: Example) -> Example:
    ...         if entity.entity_id in self._cache:
    ...             raise EntityDuplicateError(entity.entity_id)
    ...         self._cache[entity.entity_id] = entity
    ...         return entity
    ...
    >>> insert_example = ExampleInserter({})
    >>> insert_example(Example(num=1))
    Example(num=1)
    """

    @abstractmethod  # pragma: no cover
    def __call__(self, entity: Entity) -> Entity:
        """Saves entity. Should return saved entity (with id if for example
         db set one).
        """
        raise NotImplementedError


class Updater(Generic[Entity], metaclass=ABCMeta):
    """Updates stored entity with new entity version.

    :Example: Updater implementation

    >>> from acd.domain import Entity
    >>> class Example(Entity, id_attributes=('num',)):
    ...    num: int
    ...
    >>> class ExampleUpdater(Updater[Example]):
    ...     def __init__(self, cache: dict) -> None:
    ...         self._cache = cache
    ...
    ...     def __call__(self, entity: Example) -> Example:
    ...         if entity.entity_id not in self._cache:
    ...             raise EntityNotFoundError(entity.entity_id)
    ...         self._cache[entity.entity_id] = entity
    ...         return entity
    ...
    >>> entity = Example(num=1)
    >>> cache = {entity.entity_id: entity}
    >>> update_example = ExampleUpdater(cache)
    >>> update_example(Example(num=1))
    Example(num=1)
    """
    @abstractmethod  # pragma: no cover
    def __call__(self, entity: Entity) -> Entity:
        """Updates existing entity. Should return updated entity.
        """
        raise NotImplementedError


class Getter(Generic[Entity], metaclass=ABCMeta):
    """Gets stored entity.

    :Example: Getter implementation

    >>> from acd.domain import Entity
    >>> class Example(Entity, id_attributes=('num',)):
    ...    num: int
    ...
    >>> class ExampleGetter(Getter[Example]):
    ...     def __init__(self, cache: dict) -> None:
    ...         self._cache = cache
    ...
    ...     def __call__(self, entity_id: EntityId) -> Optional[Example]:
    ...         return self._cache.get(entity_id)
    ...
    >>> entity = Example(num=1)
    >>> cache = {entity.entity_id: entity}
    >>> get_example = ExampleGetter(cache)
    >>> get_example((1,))
    Example(num=1)
    """

    @abstractmethod  # pragma: no cover
    def __call__(self, entity_id: EntityId) -> Optional[Entity]:
        """Gets stored entity by entity_id or returns None.
        """
        raise NotImplementedError


class Finder(Generic[SearchRequest, Entity], metaclass=ABCMeta):
    """Finds all entities for given search request.

    :Example: Finder implementation

    >>> from acd.domain import Entity
    >>> from acd.libs.annotation import Immutable
    ...
    >>> class Example(Entity, id_attributes=('num',)):
    ...     num: int
    ...
    >>> class RangeSearchRequest(Immutable):
    ...     greater_then: int
    ...     lower_then: int
    ...
    >>> class ExampleFinder(Finder[RangeSearchRequest, Example]):
    ...     def __init__(self, cache: dict) -> None:
    ...         self._cache = cache
    ...
    ...     def __call__(self, request: RangeSearchRequest) -> List[Example]:
    ...         return [
    ...             e for e in self._cache.values()
    ...             if request.greater_then < e.num < request.lower_then
    ...         ]
    ...
    >>> entities = [
    ...     Example(num=1), Example(num=2), Example(num=3), Example(num=4),
    ... ]
    >>> cache = {e.entity_id: e for e in entities}
    >>> find_examples = ExampleFinder(cache)
    >>> request = RangeSearchRequest(greater_then=1, lower_then=4)
    >>> find_examples(request)
    [Example(num=2), Example(num=3)]
    """

    @abstractmethod  # pragma: no cover
    def __call__(self, request: SearchRequest) -> List[Entity]:
        raise NotImplementedError
