import json

import requests

from requests import Response
from typing import Optional, List, Tuple
from xml.etree import cElementTree as eTree

from msgbus import (
    FM_PARAM,
    TK_CMP_MESSAGE,
    TK_SLCTRL_OMGR_ORDERPICT,
    TK_SYS_NAK,
)
from messagebus import Message
from mbcontextmessagehandler import MbContextMessageBus

from helper import retry

from application.model.configuration import Configurations
from application.model.customexception import (
    NotFound,
    ForbiddenError,
    ProcessorError,
    InUseError,
    NotAvailableError,
    NotFoundError,
    NoResponseError,
    AlreadyUnlockedError,
    NotAllowedError,
    ClientCustomError,
)

from application.model.orderinfo import Order, OrderJsonEncoder
from util import convert_xml_to_dict


class APIRepository(object):

    def __init__(
        self,
        configs,          # type: Configurations
        message_bus,      # type: MbContextMessageBus
    ):
        # type: (...) -> None

        self.logger = configs.logger
        self.configs = configs
        self.message_bus = message_bus

    def retrieve_loyalty_customer_info(self, loyalty_customer_id):
        # type: (str) -> Optional[str, Exception]
        try:
            url = self._get_customer_url(loyalty_customer_id)
            headers = self._get_headers()
            body = dict(storeId=self.configs.store_id)
            errors_list = []

            response = retry(
                logger=self.logger,
                method_name="retrieve_loyalty_customer_info",
                n=1,
                sleep_seconds=1,
                trying_to_do=lambda: requests.request(
                    method="GET",
                    url=url,
                    json=body,
                    headers=headers,
                    timeout=5,
                ),
                is_success=lambda resp: resp.status_code == 200,
                success=lambda resp: resp,
                failed=lambda resp: errors_list.append((resp.status_code, resp))
            )
            if response:
                return response.content

            if not errors_list:
                raise NotFound()

            response_error = self._get_response_error(errors_list)
            self._raise_lock_custom_exception(response_error.status_code, response_error)
        except Exception as ex:
            self.logger.error("Error on get_benefit: {}".format(ex))
            raise

    def retrieve_benefit(
        self,
        pos_id,         # type: int
        voucher_id,     # type: str
    ):
        # type: (...) -> Optional[str, bool]

        order_info = self._get_order_info(pos_id=pos_id)
        benefit_raw = self._get_benefit(
            benefit_id=voucher_id,
            order=order_info,
        )

        if not benefit_raw:
            self.logger.info("No benefit found")
            return None

        benefit = json.loads(benefit_raw)
        benefit = benefit.get("benefit", benefit)

        notify = benefit.get("notify", False)
        benefit_id = benefit.get("id", None)
        if notify and benefit_id:
            self._lock_benefit(benefit_id=benefit_id)

        return benefit_raw

    def _get_table_info(
        self,
        pos_id,      # type: int
        table_id,    # type: int
    ):
        # type: (...) -> Optional[dict]

        if not table_id:
            return None

        params = ("TABLE_PICTURE", pos_id, table_id)
        req_buf = "\0".join([str(p) for p in params])
        message = Message(
            token=TK_CMP_MESSAGE,
            data_type=FM_PARAM,
            data=req_buf,
        )
        msg = self.message_bus.send_message(
            component='TABLEMGR',
            message=message,
        )

        if msg.token != TK_SYS_NAK:
            return convert_xml_to_dict(xml_object=eTree.XML(msg.data))
        return {}

    def _get_order_info(
        self,
        pos_id,         # type: int
        order_id="",    # type: Optional[str]
        no_cache="",    # type: Optional[str]
    ):
        # type: (...) -> dict

        params = (pos_id, order_id, 'true' if no_cache else 'false')
        req_buf = '\x00'.join((str(p) for p in params))

        message = Message(
            token=TK_SLCTRL_OMGR_ORDERPICT,
            data_type=FM_PARAM,
            data=req_buf,
        )
        msg = self.message_bus.send_message(
            component='ORDERMGR' + str(pos_id),
            message=message,
        )

        if msg.token == TK_SYS_NAK:
            return {}

        order_info = convert_xml_to_dict(xml_object=eTree.XML(msg.data.split('\0')[2]))
        custom_properties = order_info.get('CustomOrderProperties')
        table_id = custom_properties.get('TABLE_ID') if custom_properties and "TABLE_ID" in custom_properties else None
        table_info = self._get_table_info(
            pos_id=pos_id,
            table_id=table_id,
        )

        order = Order(
            store_id=self.configs.store_id,
            order_info=order_info,
            table_info=table_info,
        )
        return json.loads(json.dumps(order, encoding="utf-8", cls=OrderJsonEncoder))

    def burn_benefit(
        self,
        benefit_id,     # type: str
    ):
        # type: (...) -> None

        url = self._get_burn_url(benefit_id)
        headers = self._get_headers()
        errors_list = []

        response = retry(
            logger=self.logger,
            method_name="burn_benefit",
            n=1,
            sleep_seconds=1,
            trying_to_do=lambda: requests.request("PUT", url=url, headers=headers, timeout=20),
            is_success=lambda resp: resp.status_code == 204,
            success=lambda resp: resp,
            failed=lambda resp: errors_list.append((resp.status_code, resp))
        )
        if response:
            return response.content
        else:
            if not errors_list:
                raise NotFound()

            response_error = self._get_response_error(errors_list)
            self._raise_unlock_burn_custom_exception(response_error.status_code, response_error)

    def _get_benefit(
        self,
        benefit_id,     # type: str
        order,          # type: str
    ):
        # type: (...) -> str

        url = self._get_benefit_url(voucher_id=benefit_id)
        headers = self._get_headers()
        errors_list = []

        response = retry(
            logger=self.logger,
            method_name="get_benefit",
            n=1,
            sleep_seconds=1,
            trying_to_do=lambda: requests.request(
                "POST",
                url=url,
                headers=headers,
                timeout=20,
                json=order,
            ),
            is_success=lambda resp: resp.status_code == 200,
            success=lambda resp: resp,
            failed=lambda resp: errors_list.append((resp.status_code, resp))
        )

        if response:
            return response.content
        else:
            if not errors_list:
                raise NotFound()

            response_error = self._get_response_error(errors_list)
            self._raise_lock_custom_exception(response_error.status_code, response_error)

    def _lock_benefit(
        self,
        benefit_id  # type: str
    ):
        # type: (...) -> None

        url = self._get_lock_url(benefit_id=benefit_id)
        headers = self._get_headers()
        errors_list = []

        response = retry(
            logger=self.logger,
            method_name="lock_benefit",
            n=1,
            sleep_seconds=1,
            trying_to_do=lambda: requests.request("PUT", url=url, headers=headers, timeout=20),
            is_success=lambda resp: resp.status_code == 204,
            success=lambda resp: resp,
            failed=lambda resp: errors_list.append((resp.status_code, resp))
        )

        if response:
            return response.content
        else:
            if not errors_list:
                raise NotFound()

            response_error = self._get_response_error(errors_list=errors_list)
            self._raise_lock_custom_exception(
                status_code=response_error.status_code,
                response_error=response_error,
            )

    def unlock_benefit(
        self,
        benefit_id,     # type: str
    ):
        # type: (...) -> None

        url = self._get_unlock_url(benefit_id)
        headers = self._get_headers()
        errors_list = []

        response = retry(
            logger=self.logger,
            method_name="unlock_benefit",
            n=1,
            sleep_seconds=1,
            trying_to_do=lambda: requests.request("PUT", url=url, headers=headers, timeout=20),
            is_success=lambda resp: resp.status_code == 204,
            success=lambda resp: resp,
            failed=lambda resp: errors_list.append((resp.status_code, resp))
        )

        if response:
            return response.content
        else:
            if not errors_list:
                raise NotFound()

            response_error = self._get_response_error(errors_list)
            self._raise_unlock_burn_custom_exception(response_error.status_code, response_error)

    def _get_customer_url(self, loyalty_customer_id):
        # type: (str) -> str

        end_point_url = self.configs.endpoints.retrieve_loyalty_customer_info.format(customerId=loyalty_customer_id)
        full_url = self.configs.base_url + end_point_url

        self.logger.info("Requesting from get customer info endpoint: {}".format(full_url))

        return full_url

    def _get_benefit_url(self, voucher_id):
        # type: (str) -> str

        end_point_url = self.configs.endpoints.get_benefit.format(voucherId=voucher_id)
        full_url = self.configs.base_url + end_point_url

        self.logger.info("Requesting from get benefit endpoint: {}".format(full_url))

        return full_url

    def _get_burn_url(self, benefit_id):
        # type: (str) -> str

        end_point_url = self.configs.endpoints.burn_benefit.format(benefitId=benefit_id)
        full_url = self.configs.base_url + end_point_url

        self.logger.info("Requesting from burn endpoint: {}".format(full_url))

        return full_url

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

        end_point_url = self.configs.endpoints.lock_benefit.format(benefitId=benefit_id)
        full_url = self.configs.base_url + end_point_url

        self.logger.info("Requesting from lock endpoint: {}".format(full_url))

        return full_url

    def _get_unlock_url(self, benefit_id):
        # type: (str) -> str

        end_point_url = self.configs.endpoints.unlock_benefit.format(benefitId=benefit_id)
        full_url = self.configs.base_url + end_point_url

        self.logger.info("Requesting from unlock endpoint: {}".format(full_url))

        return full_url

    def _get_headers(self):
        # type: () -> dict

        return {"X-Api-Key": self.configs.api_key, "Content-type": "application/json"}

    def _raise_lock_custom_exception(self, status_code, response_error):
        # type: (int, Response) -> None

        if status_code == 204:
            raise NoResponseError(self.logger, response_error)
        if status_code == 403:
            raise ForbiddenError(self.logger, response_error)
        if status_code == 404:
            raise NotFoundError(self.logger, response_error)
        if status_code == 405:
            raise NotAllowedError(self.logger, response_error)
        if status_code == 406:
            raise ClientCustomError(self.logger, response_error)
        if status_code == 409:
            raise InUseError(self.logger, response_error)
        if status_code == 410:
            raise NotAvailableError(self.logger, response_error)
        if status_code >= 500:
            raise ProcessorError(self.logger, response_error)

    def _raise_unlock_burn_custom_exception(self, status_code, response_error):
        # type: (int, Response) -> None

        if status_code == 204:
            raise NoResponseError(self.logger, response_error)
        if status_code == 400:
            raise InUseError(self.logger, response_error)
        if status_code == 403:
            raise ForbiddenError(self.logger, response_error)
        if status_code == 404:
            raise NotFoundError(self.logger, response_error)
        if status_code == 405:
            raise NotAllowedError(self.logger, response_error)
        if status_code == 406:
            raise NotAvailableError(self.logger, response_error)
        if status_code == 409:
            raise AlreadyUnlockedError(self.logger, response_error)
        if status_code >= 500:
            raise ProcessorError(self.logger, response_error)

    @staticmethod
    def _get_response_error(errors_list):
        # type: (List[Tuple]) -> Response

        last_error = errors_list[-1]
        status_code, response = last_error
        return response
