from .shared.token import (
    TK_STORE_CFG_GET,
    TK_STORE_CFG_GET_FULL,
    TK_STORE_CFG_SET,
    TK_STORE_CFG_GET_COMPLETE_CONFIG,
    TK_STORE_CFG_UPDATE_COMPLETE_CONFIG,
)
from .shared.exception import (
    ConfigNotFound,
    FailedToUpdateConfig
)
from .shared.patterns import singleton
from messagebus import (
    DataType,
    DefaultToken,
    Message,
    MessageBus,
)
import json
import logging
import os
import threading
from datetime import datetime
from typing import (
    Union,
    List,
    Any,
    Dict,
)

logger = logging.getLogger("StoreConfig")


TEN_SECONDS_TIMEOUT = 10 * 1000000


@singleton
class StoreConfig(object):

    def __init__(
        self,
        message_bus,        # type: MessageBus
    ):
        # type: (...) -> None

        self.message_bus = message_bus
        self.store_config = None
        self._last_update = None
        self._cache_timeout_minutes = 5
        self._lock = threading.Lock()
        self._config_cache = {}

        if self._check_is_dev():
            logger.info(msg="Dev environment detected, warming up store config cache")
            self._consult_new_store_config()

    def get_config(
        self,
        key,        # type: str
    ):
        # type: (...) -> Union[str, List[str]]

        in_cache_value = self._config_cache.get(key)
        if in_cache_value is not None:
            return in_cache_value

        reply = self.message_bus.send_message(
            component="StoreWideConfig",
            message=Message(
                token=TK_STORE_CFG_GET,
                data_type=DataType.param,
                data=key.encode(),
                timeout=TEN_SECONDS_TIMEOUT,
            ),
        )

        if reply.token != DefaultToken.TK_SYS_ACK:
            raise ConfigNotFound()

        values = self._get_config_response(reply)
        self._config_cache[key] = values

        return values[0] if len(values) == 1 else values

    def set_config(
        self,
        key,        # type: str
        value,      # type: str
    ):
        # type: (...) -> None

        reply = self.message_bus.send_message(
            component="StoreWideConfig",
            message=Message(
                token=TK_STORE_CFG_SET,
                data_type=DataType.param,
                data=("{0}\0{1}".format(key, value)).encode(),
                timeout=TEN_SECONDS_TIMEOUT,
            ),
        )

        if reply.token != DefaultToken.TK_SYS_ACK:
            raise ConfigNotFound()

        values = self._get_config_response(reply)
        self._config_cache[key] = values

    def get_local_config(
        self,
    ):
        # type: (...) -> str

        reply = self.message_bus.send_message(
            component="StoreWideConfig",
            message=Message(token=TK_STORE_CFG_GET_FULL),
        )

        if reply.token != DefaultToken.TK_SYS_ACK:
            raise ConfigNotFound()

        return reply.data.decode()

    def get_complete_config(
        self,
    ):
        # type: (...) -> Dict

        with self._lock:
            no_cache = os.getenv("NO_CACHE") == "true"
            if no_cache or self._cache_timeout_minutes == 0:
                self._consult_new_store_config()
                return self.store_config or {}

            if self.store_config and not self._is_cache_expired():
                return self.store_config or {}

            self._call_consult_thread()
            return self.store_config or {}

    def get_config_params(
        self,
        params,     # type: List[str]
    ):
        # type: (...) -> Dict

        complete_config = self.get_complete_config()
        return {
            key: complete_config.get(key)
            for key in params
            if key in complete_config
        }

    def update_complete_config(self):
        # type: (...) -> bool

        reply = self.message_bus.send_message(
            component="StoreWideConfig",
            message=Message(token=TK_STORE_CFG_UPDATE_COMPLETE_CONFIG),
        )

        if reply.token != DefaultToken.TK_SYS_ACK:
            raise FailedToUpdateConfig()

        updated = reply.data.decode() == "true"
        if updated:
            self._consult_new_store_config()

        return updated

    def set_complete_config_value(
        self,
        key,        # type: str
        value,      # type: Any
    ):
        # type: (...) -> None

        with self._lock:
            if self.store_config is None:
                self.store_config = {}

            self.store_config[key] = value

    def clear_cache(self):
        # type: (...) -> None
        self._config_cache.clear()

    def set_store_config_cache_timeout(
        self,
        cache_timeout_minutes,      # type: int
    ):
        # type: (...) -> None

        self._cache_timeout_minutes = cache_timeout_minutes

    @staticmethod
    def _get_config_response(
        reply,      # type: Message
    ):
        # type: (...) -> Union[List[str], Any]

        values = reply.data.decode().split("\0")
        values = values[0] if len(values) == 1 else values
        return values

    def _is_cache_expired(
        self,
    ):
        # type: (...) -> bool
        return False  # TODO: Remove this line after all configs are migrated
        if not self.store_config or not self._last_update:
            return True

        return (datetime.now() - self._last_update).total_seconds() >= (
            self._cache_timeout_minutes * 60
        )

    def _consult_new_store_config(
        self,
    ):
        # type: (...) -> None

        try:
            reply = self.message_bus.send_message(
                component="StoreWideConfig",
                message=Message(token=TK_STORE_CFG_GET_COMPLETE_CONFIG),
            )

            if reply.token != DefaultToken.TK_SYS_ACK:
                raise ConfigNotFound()

            self.store_config = json.loads(reply.data.decode())
            if self.store_config is not None:
                new_timeout = self.store_config.get("Store.ApiCacheInMinutes", self._cache_timeout_minutes)
                self.set_store_config_cache_timeout(cache_timeout_minutes=int(new_timeout))
        finally:
            self._last_update = datetime.now()

    @staticmethod
    def _check_is_dev():
        # type: (...) -> bool

        dev_file = os.path.join(
            "..",
            "scripts",
            "devFile.txt",
        )
        return os.path.exists(dev_file)

    def _call_consult_thread(
        self,
    ):
        # type: (...) -> None

        thread = threading.Thread(
            target=self._consult_new_store_config,
        )
        thread.daemon = True
        thread.start()
        if self.store_config is None:
            thread.join()
