import uuid
from abc import ABC, abstractmethod
from dataclasses import asdict
from datetime import datetime
from json import JSONEncoder
from typing import List, Optional, Type

from fractal.core.event_sourcing.event import BasicSendingEvent
from fractal.core.event_sourcing.event_stream import EventStream
from fractal.core.event_sourcing.message import Message
from fractal.core.exceptions import DomainException
from fractal.core.repositories import Repository
from fractal.core.repositories.inmemory_repository_mixin import InMemoryRepositoryMixin
from fractal.core.specifications.generic.specification import Specification


class EventNotMappedError(DomainException):
    code = "EVENT_NOT_MAPPED_ERROR"
    status_code = 501

    def __init__(self, event: str):
        super(EventNotMappedError, self).__init__(
            f"Event '{event}' is not mapped to be loaded from the EventStore.",
        )


class EventStore(ABC):
    @abstractmethod
    def commit(self, event_stream: EventStream, aggregate: str, version: int):
        raise NotImplementedError

    @abstractmethod
    def get_event_stream(self) -> EventStream:
        raise NotImplementedError

    @abstractmethod
    def is_healthy(self) -> bool:
        raise NotImplementedError


class EventStoreRepository(Repository[Message], ABC):
    entity = Message


class InMemoryEventStoreRepository(
    EventStoreRepository, InMemoryRepositoryMixin[Message]
):
    pass


class BasicEventStore(EventStore, ABC):
    def __init__(self, event_store_repository: EventStoreRepository):
        self.event_store_repository = event_store_repository

    def is_healthy(self) -> bool:
        return self.event_store_repository.is_healthy()


class ObjectEventStore(BasicEventStore):
    def commit(self, event_stream: EventStream, aggregate: str, version: int):
        for event in event_stream.events:
            self.event_store_repository.add(
                Message(
                    id=str(uuid.uuid4()),
                    occurred_on=datetime.utcnow(),
                    event=event.__class__.__name__,
                    data=event,
                    object_id=event.object_id,
                    aggregate_root_id=event.aggregate_root_id,
                )
            )

    def get_event_stream(
        self, specification: Optional[Specification] = None
    ) -> EventStream:
        return EventStream(
            events=list(
                map(lambda m: m.data, self.event_store_repository.find(specification))
            )
        )


class DictEventStore(BasicEventStore):
    def __init__(
        self,
        event_store_repository: EventStoreRepository,
        events: List[Type[BasicSendingEvent]],
    ):
        super(DictEventStore, self).__init__(event_store_repository)
        self.events = {e.__name__: e for e in events}

    def commit(self, event_stream: EventStream, aggregate: str, version: int):
        for event in event_stream.events:
            self.event_store_repository.add(
                Message(
                    id=str(uuid.uuid4()),
                    occurred_on=datetime.utcnow(),
                    event=event.__class__.__name__,
                    data=asdict(event),
                    object_id=event.object_id,
                    aggregate_root_id=event.aggregate_root_id,
                )
            )

    def get_event_stream(
        self, specification: Optional[Specification] = None
    ) -> EventStream:
        events = []
        for m in self.event_store_repository.find(specification):
            if event := self.events.get(m.event, None):
                events.append(event(**m.data))
            else:
                raise EventNotMappedError(m.event)
        return EventStream(events=events)


class JsonEventStore(BasicEventStore):
    def __init__(
        self,
        event_store_repository: EventStoreRepository,
        events: List[Type[BasicSendingEvent]],
        json_encoder: Optional[Type[JSONEncoder]] = None,
    ):
        super(JsonEventStore, self).__init__(event_store_repository)
        self.events = {e.__name__: e for e in events}
        self.json_encoder = json_encoder

    def commit(self, event_stream: EventStream, aggregate: str, version: int):
        import json

        for event in event_stream.events:
            self.event_store_repository.add(
                Message(
                    id=str(uuid.uuid4()),
                    occurred_on=datetime.utcnow(),
                    event=event.__class__.__name__,
                    data=json.dumps(asdict(event), cls=self.json_encoder),
                    object_id=event.object_id,
                    aggregate_root_id=event.aggregate_root_id,
                )
            )

    def get_event_stream(
        self, specification: Optional[Specification] = None
    ) -> EventStream:
        import json

        events = []
        for m in self.event_store_repository.find(specification):
            if event := self.events.get(m.event, None):
                events.append(event(**json.loads(m.data)))
            else:
                raise EventNotMappedError(m.event)
        return EventStream(events=events)


class PickleEventStore(BasicEventStore):
    def __init__(
        self,
        event_store_repository: EventStoreRepository,
        events: List[Type[BasicSendingEvent]],
    ):
        super(PickleEventStore, self).__init__(event_store_repository)
        self.events = {e.__name__: e for e in events}

    def commit(self, event_stream: EventStream, aggregate: str, version: int):
        import pickle

        for event in event_stream.events:
            self.event_store_repository.add(
                Message(
                    id=str(uuid.uuid4()),
                    occurred_on=datetime.utcnow(),
                    event=event.__class__.__name__,
                    data=pickle.dumps(event),
                    object_id=event.object_id,
                    aggregate_root_id=event.aggregate_root_id,
                )
            )

    def get_event_stream(
        self, specification: Optional[Specification] = None
    ) -> EventStream:
        import pickle

        events = []
        for m in self.event_store_repository.find(specification):
            events.append(pickle.loads(m.data))
        return EventStream(events=events)
