import sys
import json

from typing import (
    Dict,
    List,
    Sequence,
    Union,
    Optional,
)

from .dto import SuggestiveSelling
from .dto.exception import (
    ProductBarcodeNotFound,
    ProductHasNotFilledMinQuantity,
)
from .error import (
    ProductError,
    UpdateItemsError,
)
from .shared.dto import RuptureEventDto
from .shared.dto.exception import InvalidPartCode
from .token import ProductApiTokens

from messagebus import (
    ApplicationErrorMessage,
    MessageBus,
    Message,
    DataType,
    DefaultToken,
)


class ProductService(object):

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

        self.message_bus = message_bus
        self.component_name = "ORDERMGR"

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

        msg = self._get_tenders()
        json_data = json.loads(msg.data)
        return _byteify(json_data)

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

        msg = self._get_tenders()
        tenders = json.loads(msg.data)
        tenders_dict = {}
        for tender in tenders:
            tenders_dict[tender["id"]] = tender

        return _byteify(tenders_dict)

    def get_products_name(
        self,   # type: ProductService
    ):
        # type: (...) -> Dict[str, str]

        reply = self.message_bus.send_message(
            component=self.component_name,
            message=Message(token=ProductApiTokens.TK_SLCTRL_PMGR_GET_PRODUCTS_NAME)
        )

        if reply.token != DefaultToken.TK_SYS_ACK:
            raise RuntimeError("Error getting products name")

        return json.loads(reply.data.decode())

    def get_products(
        self,           # type: ProductService
        pos_id=None,    # type: Optional[int]
    ):
        # type: (...) -> (Dict[str, Dict], Dict[str, Dict])

        reply = self.message_bus.send_message(
            component=self.component_name,
            message=Message(
                token=ProductApiTokens.TK_SLCTRL_PMGR_GET_PRODUCTS,
                data=pos_id,
            ),
        )

        if reply.token != DefaultToken.TK_SYS_ACK:
            raise RuntimeError("Error getting products name")

        product_lists = json.loads(reply.data.decode())
        product_list = product_lists[0]
        product_by_barcode = product_lists[1]

        return product_list, product_by_barcode

    def update_items(
        self,           # type: ProductService
        user_id,        # type: int
        enabled,        # type: Sequence[int]
        disabled,       # type: Sequence[int]
    ):
        # type: (...) -> RuptureEventDto

        reply = self.message_bus.send_message(
            component=self.component_name,
            message=Message(
                token=ProductApiTokens.TK_SLCTRL_PMGR_RUPTURE_UPDATE_ITEMS,
                data=json.dumps({
                    "user_id": user_id,
                    "enabled": enabled,
                    "disabled": disabled,
                }).encode()
            )
        )

        if reply.token == DefaultToken.TK_SYS_APPLICATION_ERROR:
            application_error = ApplicationErrorMessage.build_from_message(message=reply)
            if application_error.error_code == UpdateItemsError.INVALID_PART_CODE.code:
                raise InvalidPartCode(part_code=int(application_error.error_message))

        data_dict = json.loads(reply.data.decode())
        return RuptureEventDto.build_from_dict(data_dict)

    def update_daily_dishes(
        self,       # type: ProductService
        user_id,    # type: int
        to_add,     # type: List[Dict[str, int]]
        to_remove,  # type: List[int]
    ):
        # type: (...) -> None

        dishes_changes_json = json.dumps({
            "userId": user_id,
            "unset": to_remove,
            "set": to_add,
        }).encode()

        reply = self.message_bus.send_message(
            component=self.component_name,
            message=Message(
                token=ProductApiTokens.TK_SLCTRL_PMGR_RUPTURE_UPDATE_DAILY_DISHES,
                data=dishes_changes_json,
            ),
        )

        if reply.token != DefaultToken.TK_SYS_ACK:
            raise RuntimeError("Could not update daily dishes")

        return None

    def get_product_info_by_barcode(
        self,                                   # type: ProductService
        pos_id,                                 # type: int
        sale_type,                              # type: int
        service_period,                         # type: str
        barcode,                                # type: str
        with_filled_mandatory_options=False,    # type: bool
    ):
        # type: (...) -> Dict[str, Union[Dict, List, int, str, float, bool]]

        request = json.dumps({
            "posId": pos_id,
            "saleType": sale_type,
            "servicePeriod": service_period,
            "barcode": barcode,
            "withFilledMandatoryOptions": with_filled_mandatory_options,
        }).encode()

        reply = self.message_bus.send_message(
            component=self.component_name,
            message=Message(
                token=ProductApiTokens.TK_SLCTRL_PMGR_GET_PRODUCT_INFO,
                data=request,
            ),
        )

        if reply.token == DefaultToken.TK_SYS_APPLICATION_ERROR:
            application_error = ApplicationErrorMessage.build_from_message(message=reply)
            if application_error.error_code == ProductError.PRODUCT_BARCODE_NOT_FOUND.code:
                raise ProductBarcodeNotFound(part_code=application_error.error_message)
            else:
                raise ProductHasNotFilledMinQuantity(
                    part_code=int(application_error.error_message),
                    part_name=self._get_part_name(application_error.error_message),
                )

        if reply.token != DefaultToken.TK_SYS_ACK:
            raise RuntimeError("Could not update daily dishes")

        return json.loads(reply.data)

    def get_suggestive_selling(
        self,       # type: ProductService
    ):
        # type: (...) -> SuggestiveSelling

        reply = self.message_bus.send_message(
            component=self.component_name,
            message=Message(token=ProductApiTokens.TK_SLCTRL_PMGR_GET_SUGGESTIVE_SELL),
        )

        if reply.token != DefaultToken.TK_SYS_ACK:
            raise RuntimeError('Error getting suggestive sell')

        return SuggestiveSelling.from_dict(json.loads(reply.data))

    def get_images_hashes(
        self,    # type: ProductService
    ):
        # type: (...) -> Dict[str, Dict[str, str]]

        reply = self.message_bus.send_message(
            component=self.component_name,
            message=Message(
                token=ProductApiTokens.TK_SLCTRL_PMGR_GET_IMAGES_HASHES,
            ),
        )

        if reply.token != DefaultToken.TK_SYS_ACK:
            raise RuntimeError("Error getting images hashes")

        return json.loads(reply.data.decode())

    def _get_tenders(
        self,       # type: ProductService
    ):
        # type: (...) -> Message

        msg = self.message_bus.send_message(
            component=self.component_name,
            message=Message(
                token=ProductApiTokens.TK_SLCTRL_PMGR_GET_TENDERS,
                data_type=DataType.param,
            )
        )

        if msg.token != DefaultToken.TK_SYS_ACK:
            raise RuntimeError("Could not retrieve tenders")

        return msg

    def _get_part_name(
        self,       # type: ProductService
        part_code,  # type: str
    ):
        # type: (...) -> str

        products_name = self.get_products_name()

        return products_name.get(part_code, part_code)


def _byteify(input):
    if sys.version_info[0] >= 3:
        return input

    if isinstance(input, dict):
        return {_byteify(key): _byteify(value) for key, value in input.iteritems()}

    if isinstance(input, list):
        return [_byteify(element) for element in input]

    if isinstance(input, str):
        return input.encode()

    if isinstance(input, type(u"")):
        return input.encode("utf-8")

    return input
