# encoding: utf-8

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,
    MessageBus,
)

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,
    ResponseError,
    TranslatedErrorMessage,
    GoneError,
)

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: MessageBus
    ):
        # type: (...) -> None

        self.logger = configs.logger
        self.configs = configs
        self.message_bus = message_bus
        self.exception_map = {
            204: NoResponseError,
            403: ForbiddenError,
            404: NotFoundError,
            405: NotAllowedError,
            410: GoneError,
            500: ProcessorError,
        }
        self.per_method_exception_map = {
            "lock": {
                409: InUseError,
                406: ClientCustomError,
            },
            "burn": {
                400: InUseError,
                406: NotAvailableError,
                409: AlreadyUnlockedError
            }
        }

    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=self.configs.partner_retry_policy.count,
                sleep_seconds=self.configs.partner_retry_policy.sleep,
                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)),
                on_exception=lambda ex: errors_list.append(ex)
            )
            if response is not None:
                return response.content

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

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

        order_picture = self.get_order_picture(
            pos_id=pos_id,
        )
        table_picture = self._get_table_picture_associated_with_order_picture(pos_id, order_picture)
        order_info = self._build_order_info(
            order_picture=order_picture,
            table_picture=table_picture,
        )

        self.logger.info("Order json sent on retrieve:\n{}".format(order_info))
        benefit_raw = self._get_benefit(
            benefit_id=coupon_id,
            order=order_info,
        )

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

        benefit = json.loads(benefit_raw)
        self.logger.info("Json from partner: {}".format(benefit))
        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,
                pos_id=pos_id,
            )

        return benefit_raw

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

        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 msg.data
        return None

    def get_order_picture(
        self,               # type: APIRepository
        pos_id,             # type: int
        order_id='',        # type: Optional[str]
        no_cache=None,      # type: Optional[str]
    ):
        # type: (...) -> Optional[eTree]

        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 None

        return msg.data.split('\0')[2]

    def _get_table_picture_associated_with_order_picture(
        self,
        pos_id,
        order_picture
    ):
        table_picture = None
        order_info = convert_xml_to_dict(xml_object=eTree.XML(order_picture))
        custom_properties = order_info.get('CustomOrderProperties')
        table_id = custom_properties.get('TABLE_ID') if custom_properties and "TABLE_ID" in custom_properties else None
        if table_id is not None:
            table_picture = self._get_table_picture(
                pos_id=pos_id,
                table_id=table_id,
            )
        return table_picture

    def _build_order_info(
        self,
        order_picture,          # type: str
        table_picture=None,     # type: Optional[str]
    ):
        # type: (...) -> dict

        order_info = convert_xml_to_dict(xml_object=eTree.XML(order_picture))
        table_info = None
        if table_picture:
            table_info = convert_xml_to_dict(xml_object=eTree.XML(table_picture))

        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
        order_picture,      # type: str
        table_picture,      # type: Optional[str]
    ):
        # type: (...) -> None

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

        body_json = self._build_order_info(
            order_picture=order_picture,
            table_picture=table_picture,
        )
        self.logger.debug("Order json sent on burn:\n{}".format(body_json))

        response = retry(
            logger=self.logger,
            method_name="burn_benefit",
            n=self.configs.partner_retry_policy.count,
            sleep_seconds=self.configs.partner_retry_policy.sleep,
            trying_to_do=lambda: requests.request(
                method="PUT",
                url=url,
                headers=headers,
                json=body_json,
                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 is not None:
            return

        if not errors_list:
            raise NotFound()

        response_error = self._get_response_error(errors_list)
        raise self._build_unlock_burn_custom_exception(
            status_code=response_error.status_code,
            response_error=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)
            raise self._build_lock_custom_exception(
                status_code=response_error.status_code,
                response_error=response_error,
            )

    def lock_benefit(
        self,                   # type: APIRepository
        benefit_id,             # type: str
        pos_id,                 # type: int
    ):
        # type: (...) -> None

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

        order_picture = self.get_order_picture(
            pos_id=pos_id,
        )
        table_picture = self._get_table_picture_associated_with_order_picture(pos_id, order_picture)

        body_json = self._build_order_info(
            order_picture=order_picture,
            table_picture=table_picture,
        )

        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, json=body_json, 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)
            raise self._build_lock_custom_exception(
                status_code=response_error.status_code,
                response_error=response_error,
            )

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

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

        body_json = self._build_order_info(
            order_picture=order_picture,
            table_picture=table_picture,
        )

        response = retry(
            logger=self.logger,
            method_name="unlock_benefit",
            n=self.configs.partner_retry_policy.count,
            sleep_seconds=self.configs.partner_retry_policy.sleep,
            trying_to_do=lambda: requests.request(
                method="PUT",
                url=url,
                json=body_json,
                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 is not None:
            return

        if not errors_list:
            raise NotFound()

        response_error = self._get_response_error(errors_list)
        raise self._build_unlock_burn_custom_exception(
            status_code=response_error.status_code,
            response_error=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 _build_per_method_custom_exception(
        self,            # type: APIRepository
        method_name,     # type: str
        status_code,     # type: int
        response_error,  # type: Response
    ):
        # type: (...) -> Optional[Exception]

        exception_cls = self.exception_map.get(
            status_code,
            self.per_method_exception_map.get(method_name, ResponseError).get(status_code, ResponseError)
        )
        if exception_cls == ClientCustomError:
            return self._build_client_custom_error(response_error=response_error)

        return exception_cls(
            logger=self.logger,
            response=response_error,
        )

    def _build_client_custom_error(
        self,            # type: APIRepository
        response_error,  # type: Response
    ):
        try:
            body = response_error.json()
            return ClientCustomError(
                message=TranslatedErrorMessage(
                    pt=body["message"].get("pt"),
                    en=body["message"].get("en"),
                    es=body["message"].get("es"),
                ),
            )
        except Exception as ex:
            self.logger.exception(
                msg="Invalid response body from partner - {}".format(response_error.content),
                exc_info=ex
            )
            return ClientCustomError(
                message=TranslatedErrorMessage(
                    pt=None,
                    en=None,
                    es=None,
                )
            )

    def _build_lock_custom_exception(
        self,            # type: APIRepository
        status_code,     # type: int
        response_error,  # type: Response
    ):
        # type: (...) -> Exception

        return self._build_per_method_custom_exception(
            method_name="lock",
            status_code=status_code,
            response_error=response_error,
        )

    def _build_unlock_burn_custom_exception(
        self,            # type: APIRepository
        status_code,     # type: int
        response_error,  # type: Response
    ):
        # type: (...) -> Exception

        return self._build_per_method_custom_exception(
            method_name="burn",
            status_code=status_code,
            response_error=response_error,
        )

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

        for i in range(len(errors_list) - 1, -1, -1):
            last_error = errors_list[-i]
            if not isinstance(last_error, Exception):
                status_code, response = last_error
                return response

        raise errors_list[-1]
