import logging
from abc import (
    ABC,
    abstractmethod,
)
from threading import (
    Condition,
    Thread,
)
from typing import (
    Any,
    Dict,
    List,
    Optional,
    Tuple,
)

from contextmessagehandler import ContextMessageHandlerBuilder
from messagebus import MessageBus

logger = logging.getLogger(__name__)


class AutoupdateContextMessageHandlerBuilder(ContextMessageHandlerBuilder, ABC):
    def __init__(self, message_bus: MessageBus) -> None:
        super().__init__(message_bus=message_bus)
        self.ttl: float = 0
        self.update_context_thread: Optional[Thread] = None
        self.current_dict = None
        self.running = False
        self.sleep_condition = Condition()

    def build_context(self):
        self.ttl = self.get_context_ttl()
        if self.ttl > 0:
            self.running = True
            self.update_context_thread = Thread(target=self._context_updater)
            self.update_context_thread.daemon = True
            self.update_context_thread.start()

        self.current_dict = self.build_context_dict()
        return self.current_dict

    def build_singletons_with_context(self, context_dict):
        self.build_context_independent_singletons()

        self.context_will_update()
        for context_name in self.current_dict:
            self.build_context_singletons(
                context_name=context_name,
                context_dict=self.current_dict[context_name],
            )
        self.context_updated()

    def destroy_singletons_with_context(self, context_dict):
        self.running = False
        with self.sleep_condition:
            self.sleep_condition.notify()
        if self.update_context_thread is not None:
            self.update_context_thread.join()

        self.context_will_update()
        for context_name in self.current_dict:
            self.destroy_context_singletons(context_name=context_name)
        self.context_updated()

        self.destroy_context_independent_singletons()

    def _context_updater(self):
        while self.running:
            try:
                with self.sleep_condition:
                    self.sleep_condition.wait(timeout=self.ttl)
                if not self.running:
                    return

                new_dict = self.build_context_dict()
                additions, removals = self._get_changes(new_dict=new_dict)
                if len(additions) == 0 and len(removals) == 0:
                    continue

                self.context_will_update()
                self._add_new_contexts(additions=additions)
                self._remove_old_contexts(removals=removals)
                self.current_dict = new_dict
                self.context_updated()
            except (Exception,):
                logger.exception("Error updating context")

    def _get_changes(self, new_dict) -> Tuple[Dict[str, Any], List[str]]:
        additions = {}
        for context_name in new_dict:
            if context_name not in self.current_dict:
                additions[context_name] = new_dict[context_name]

        removals = []
        for context_name in self.current_dict:
            if context_name not in new_dict:
                removals.append(context_name)

        return additions, removals

    def _add_new_contexts(self, additions: Dict[str, Any]):
        for context_name in additions.keys():
            self.build_context_singletons(
                context_name=context_name,
                context_dict=additions[context_name],
            )
            self.add_context(
                context_name=context_name,
                created_context=additions[context_name],
            )

    def _remove_old_contexts(self, removals: List[str]):
        for context_name in removals:
            self.destroy_context_singletons(context_name=context_name)
            self.remove_context(context_name=context_name)

    def build_context_independent_singletons(self):
        return

    @abstractmethod
    def get_context_ttl(self) -> float:
        raise NotImplementedError()

    @abstractmethod
    def build_context_dict(self) -> Dict[str, Any]:
        raise NotImplementedError()

    def context_will_update(self):
        return

    @abstractmethod
    def build_context_singletons(self, context_name: str, context_dict: Dict[str, Any]) -> None:
        raise NotImplementedError()

    @abstractmethod
    def destroy_context_singletons(self, context_name) -> Any:
        raise NotImplementedError()

    def context_updated(self):
        return

    def destroy_context_independent_singletons(self):
        return
