import logging
import threading
from threading import Lock
from abc import (
    ABC,
    abstractmethod,
)
from typing import (
    Dict,
    Generic,
    Optional,
    TypeVar,
)

from contextmessagebus import (
    Context,
    ContextMessageBus,
    MultiContextMessageBus,
)
from messagebus import (
    Event,
    Message,
    MessageBus,
)
from messagehandler import (
    MessageHandler,
    MessageHandlerBuilder,
)

logger = logging.getLogger("DynamicContextMessageHandlerBuilder")

T = TypeVar("T")

class DynamicContextMessageHandlerBuilder(MessageHandlerBuilder, Generic[T], ABC):

    def __init__(self, message_bus: Optional[MessageBus] = None) -> None:
        self.message_bus = message_bus
        self.context_dict: Dict[str, Optional[Context]] = {}
        self.message_handler_context = {}

        self._cache_lock = Lock()
        self._lock_dict = {}

    def build_singletons(self):
        pass

    def build_message_handler(
        self,
        message: Optional[Message],
        event: Optional[Event],
    ) -> MessageHandler:
        try:
            context_name = self.get_context_name(message, event)
        except KeyError:
            logger.error("The 'context' was not present in the message or event: message: %s event: %s", message, event)
            context_name = None

        setattr(threading.current_thread(), '_edp_context_name', context_name)
        if isinstance(self.message_bus, MultiContextMessageBus):
            self.message_bus.set_current_context(context_name)

        context = self._get_context(context_name)

        if context is not None:
            request_data = self.get_request_context_data(message, event)
            if request_data is not None:
                setattr(context, "request_data", request_data)

            message_handler = self.build_message_handler_with_context(context)
            self.message_handler_context[message_handler] = context_name
            return message_handler

        logger.error("Could not retrieve context %s. Message: %s. Event: %s.", context_name, message, event)

        raise Exception("No context")

    def _get_context_lock(self, context_name: str) -> Lock:
        with self._cache_lock:
            if context_name not in self._lock_dict:
                self._lock_dict[context_name] = Lock()
            return self._lock_dict[context_name]

    def _get_context(self, context_name: Optional[str]) -> Optional[Context]:
        if context_name is None:
            return None

        if context_name not in self.context_dict:
            with self._get_context_lock(context_name):
                if context_name not in self.context_dict:
                    context_config = self.build_context(context_name)
                    if context_config is None:
                        logger.error("build_context returned None for context '%s'. Skipping cache.", context_name)
                        return None
                    self.add_context(
                        context_name=context_name,
                        context_config=context_config,
                    )

        return self.context_dict[context_name]

    @abstractmethod
    def build_context(self, context_name: str) -> T:
        raise NotImplementedError()


    def add_context(
        self,
        context_name: str,
        context_config: T,
    ):
        context_data = None
        if context_config is not None:
            context_message_bus = ContextMessageBus(context_name, self.message_bus)
            context_data = Context(
                context_name=context_name,
                message_bus=context_message_bus,
                context_config=context_config,
            )

        self.context_dict[context_name] = context_data

    def destroy_message_handler(self, message_handler):
        if hasattr(threading.current_thread(), '_edp_context_name'):
            delattr(threading.current_thread(), '_edp_context_name')

        context_name = self.message_handler_context[message_handler]
        self.destroy_message_handler_with_context(self.context_dict.get(context_name), message_handler)
        del self.message_handler_context[message_handler]

    def destroy_singletons(self):
        created_contexts = {}

        for context_name, context in self.context_dict.items():
            if context is not None:
                created_contexts[context_name] = context

        self.destroy_singletons_with_context(created_contexts)

    def get_request_context_data(
        self,
        message: Optional[Message],
        event: Optional[Event],
    ) -> Optional[Dict[str, str]]:
        return None

    def remove_context(
        self,
        context_name: str,
    ):
        del self.context_dict[context_name]

    @abstractmethod
    def build_message_handler_with_context(self, context: Context[T]):
        raise NotImplementedError()

    @abstractmethod
    def destroy_message_handler_with_context(
        self,
        context: Optional[Context[T]],
        message_handler: MessageHandler,
    ) -> None:
        raise NotImplementedError()

    @abstractmethod
    def destroy_singletons_with_context(self, context_dict: Dict[str, Context[T]]) -> None:
        raise NotImplementedError()

    @staticmethod
    def get_context_name(message, event):
        if message is not None:
            return message.headers["context"]
        else:
            return event.headers["context"]