import json
from datetime import datetime
from logging import Logger
from memorycache import CacheManager
from string import upper

import msgbus
from mbcontextmessagehandler import MbContextMessageBus
from typing import (
    Any,
    Dict,
    Optional,
)

from messagebus import (
    DefaultToken,
    Message,
)
from model import (
    CatalogVersionDto,
    CacheKeys,
)

from model.config import Config
from model.error_type import (
    ExternalServicesErrorsType,
    PosValidationErrorsType,
)
from model.exception import (
    ExternalServiceErrors,
    ValidationError,
)
from model.repository import PosRepositoryModel
from interactor.route.store.get_status.dto import (
    GetStoreResponse,
    GetStoreRequest,
    StatusResponse,
)
from model.tokens import BusTokens
from validation_util import validate_api_key


class GetStoreInteractor(object):
    def __init__(
        self,
        logger,                 # type: Logger
        config,                 # type: Config
        pos_repository,         # type: PosRepositoryModel
        message_bus,            # type: MbContextMessageBus
        memory_cache,           # type: Dict[str, Any[Dict[str, CacheManager], CacheManager]]
    ):
        # type: (...) -> None

        self.logger = logger
        self.config = config
        self.pos_repository = pos_repository
        self.message_bus = message_bus
        self.memory_cache = memory_cache

    def execute(
        self,
        request,    # type: GetStoreRequest
    ):
        # type: (...) -> GetStoreResponse

        validate_api_key(request, self.config.api_key_mapping)

        catalog_version_data = self._get_cached_value(
            key=CacheKeys.MENU_KEY,
            func=self.pos_repository.get_catalog_version,
        )
        if not catalog_version_data.menu_id:
            raise ExternalServiceErrors(ExternalServicesErrorsType.UnableToGetMenuId)

        fiscal = self._get_cached_value(
            key=CacheKeys.FISCAL_KEY,
            func=self._fiscal_status,
        )

        pos_id = str(request.pos_id)
        printer = self._get_cached_pos_value(
            pos_id=pos_id,
            key=CacheKeys.PRINTER_KEY,
            func=self._get_printer_status,
        )
        eft = self._get_cached_pos_value(
            pos_id=pos_id,
            key=CacheKeys.EFT_KEY,
            func=self._get_eft_status,
        )
        manager = self._get_cached_pos_value(
            pos_id=pos_id,
            key=CacheKeys.MANAGER_KEY,
            func=self._get_manager_status,
        )

        service_period = self._get_service_period(pos_id=pos_id)

        return GetStoreResponse(
            menu_id=catalog_version_data.menu_id,
            menu_version=catalog_version_data.menu_version,
            menu_name=catalog_version_data.menu_name,
            printer=printer,
            fiscal=fiscal,
            eft=eft,
            manager=manager,
            service_period=service_period,
        )

    def _get_cached_value(
        self,
        key,    # type: str
        func,   # type: Any
    ):
        # type: (...) -> object

        value = self.memory_cache[key].get_cached_object(new_object_func=func)
        if value is None or (hasattr(value, "status") and value.status is False):
            self.memory_cache[key].force_renew_cache(new_object_func=func)
            self.logger.info('Updating cache data for {}'.format(key.upper()))

        message = self._get_logger_message(value=value)
        self.logger.info('Cache Memory Value for {}: {}'.format(key.upper(), message))
        return value

    def _get_cached_pos_value(
        self,
        pos_id,     # type: str
        key,        # type: str
        func,       # type: Any
    ):
        # type: (...) -> object

        value = self.memory_cache[key][pos_id].get_cached_object(func, False, pos_id)
        if value is None or (hasattr(value, "status") and value.status is False):
            self.memory_cache[key][pos_id].force_renew_cache(func, pos_id)
            self.logger.info('Updating cache data for: {}'.format(key))

        message = self._get_logger_message(value=value)
        self.logger.info('Cache Memory Value for {}: {}'.format(key.upper(), message))
        return value

    def _fiscal_status(
        self,
    ):
        # type: (...) -> StatusResponse

        try:
            if self.config.nf_type == 'NFCE':
                status_fiscal_response = self.pos_repository.get_sefaz_status()
            elif self.config.nf_type in ('SAT', 'MFE'):
                status_fiscal_response = self.pos_repository.get_store_sat_status()
            else:
                message = "NF type ({}) is invalid".format(self.config.nf_type)
                self.logger.warning(message)
                return StatusResponse(
                    status=False,
                    message=message,
                )

            self.logger.info('Fiscal Status: {}'.format(status_fiscal_response.status))
            return StatusResponse(
                status=status_fiscal_response.status,
                message=status_fiscal_response.message,
            )
        except Exception as ex:
            self.logger.exception('Error get fiscal status: {}'.format(ex.message))
            return StatusResponse(
                status=False,
                message=ex.message,
            )

    def _get_printer_status(
        self,
        pos_id,     # type: int
    ):
        # type: (...) -> StatusResponse

        try:
            device_name = self._get_device_manager_info(
                pos_id=pos_id,
                device_type="printer",
            )
            if device_name is None:
                message = 'Printer unavailable in POS{}. Device not found.'.format(pos_id)
                self.logger.error(message)
                raise ValidationError(PosValidationErrorsType.DeviceNotFound)

            message_printer = Message(
                token=msgbus.TK_PRN_GETSTATUS,
                data_type=msgbus.FM_PARAM,
                data=None,
            )
            msg_printer = self.message_bus.send_message(
                component=device_name,
                message=message_printer,
            )

            if msg_printer.token == DefaultToken.TK_SYS_ACK and msg_printer.data == "0":
                self.logger.info('Printer status OK: {}'.format(msg_printer.data))
                return StatusResponse(
                    status=True,
                    message="OK",
                )

            self.logger.warning('Printer status: {}'.format(msg_printer.data))
            return StatusResponse(
                status=False,
                message=msg_printer.data,
            )
        except (Exception, ValidationError) as ex:
            self.logger.error('Error getting printer status in POS{}. Error info: {}'.format(pos_id, ex.message))
            return StatusResponse(
                status=False,
                message="NOK",
            )

    def _get_eft_status(
        self,
        pos_id,     # type: int
    ):
        # type: (...) -> StatusResponse

        try:
            device_name = self._get_device_manager_info(
                pos_id=pos_id,
                device_type="eft",
            )
            if device_name is None:
                message = 'Eft unavailable in POS{} Device not found.'.format(pos_id)
                self.logger.error(message)
                raise ValidationError(PosValidationErrorsType.EftDeviceNotFound)

            data = "{};{};{}".format(pos_id, datetime.now().strftime("%Y%m%d"), datetime.now().strftime('%H:%M:%S'))
            message = Message(
                token=BusTokens.TK_ELECTRONIC_PAYMENT_CONNECTIVITY_TEST,
                data_type=msgbus.FM_PARAM,
                data=data,
            )
            msg = self.message_bus.send_message(
                component=device_name,
                message=message,
            )

            if msg.token == DefaultToken.TK_SYS_ACK and upper(msg.data) == "TRUE":
                self.logger.info('Eft status OK: {}'.format(msg.data))
                return StatusResponse(
                    status=True,
                    message="OK",
                )

            self.logger.warning('Eft status: {}'.format(msg.data))
            return StatusResponse(
                status=False,
                message="NOK",
            )

        except (Exception, ValidationError) as ex:
            self.logger.error('Error getting eft status in POS{}. Error info: {}'.format(pos_id, ex.message))
            return StatusResponse(
                status=False,
                message="NOK",
            )

    def _get_manager_status(
        self,
        pos_id,     # type: int
    ):
        # type: (...) -> StatusResponse

        totem_was_closed = self.pos_repository.get_manager_status(pos_id=pos_id)
        model = self.pos_repository.get_model(pos_id=pos_id)
        message = self.pos_repository.translate_message(
            model=model,
            message="CLOSED" if totem_was_closed else "OPENED",
        )
        self.logger.info('Manager status: {}'.format(message))
        return StatusResponse(
            status=not totem_was_closed,
            message=message,
        )

    def _get_device_manager_info(
        self,
        pos_id,         # type: int
        device_type,    # type: str
    ):
        # type: (...) -> Optional[str]

        request = json.dumps(
            {
                "posId": pos_id,
                "deviceType": device_type
            }
        )
        message = Message(
            token=BusTokens.TK_GET_USED_SERVICE,
            data_type=msgbus.FM_PARAM,
            data=request,
        )
        msg = self.message_bus.send_message(
            component="DeviceManager",
            message=message,
        )

        if msg.token == DefaultToken.TK_SYS_ACK:
            self.logger.info('Getting {} info: {}'.format(device_type.upper(), msg.data))
            return msg.data

        self.logger.warning('Device manager info: {} Token: {}'.format(msg.data, msg.token))
        return None

    def _get_service_period(
        self,       # type: GetStoreInteractor
        pos_id,     # type: int
    ):
        # type: (...) -> Optional[str]

        model = self.pos_repository.get_model(pos_id=pos_id)
        service_period = self.pos_repository.get_custom(
            model=model,
            key="SERVICE_PERIOD",
        )

        if not service_period:
            return None

        service_period = json.loads(service_period or "{}")
        service_period = service_period.get("servicePeriodId", None)
        service_period = str(service_period) if service_period is not None else ""
        self.memory_cache[CacheKeys.SERVICE_PERIOD_KEY][pos_id] = service_period
        return service_period

    @staticmethod
    def _get_logger_message(
        value,  # type: Any[CatalogVersionDto, StatusResponse]
    ):
        # type: (...) -> str

        if value is None:
            return 'Value is empty'

        if isinstance(value, CatalogVersionDto):
            return 'Menu Id: {}, Menu Version: {}, Menu Name: {}'.format(
                value.menu_id,
                value.menu_version,
                value.menu_name
            )

        return value.message
