# encoding: utf-8

import json
from xml.etree import cElementTree as eTree

from msgbus import (
    FM_PARAM,
    TK_CMP_MESSAGE,
    TK_SLCTRL_OMGR_ORDERPICT,
    TK_SYS_NAK,
)

import requests
from application.model import (
    ApplierSubTypes,
    TotaledMessage,
    TranslatedMessage,
)
from application.model.configuration import Configurations
from application.model.customexception import (
    NotFound,
    ForbiddenError,
    ProcessorError,
    InUseError,
    NotAvailableError,
    NotFoundError,
    NoResponseError,
    AlreadyUnlockedError,
    NotAllowedError,
    ClientCustomError,
    ResponseError,
    GoneError,
    ServiceNotFoundError,
)
from application.model.orderinfo import (
    Order,
    OrderJsonEncoder,
)
from helper import retry
from messagebus import (
    Message,
    MessageBus,
)
from requests import Response
from typing import (
    Any,
    Optional,
    List,
    Tuple,
    Dict,
    Union,
)
from util import get_applier_sub_type_from_order
from posentitiesformatter import (
    OrderFormatter,
    TableFormatter,
)


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,
            206: NoResponseError,
            403: ForbiddenError,
            404: NotFoundError,
            405: NotAllowedError,
            410: GoneError,
            500: ProcessorError,
        }
        self.per_method_exception_map = {
            "lock": {
                404: NotFoundError,
                406: ClientCustomError,
                409: InUseError,
            },
            "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(applier_sub_type=ApplierSubTypes.LOYALTY)
            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.message.encode("utf-8")))
            raise

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

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

        self.logger.info("Order json sent on retrieve:\n{}".format(json.dumps(order_info)))
        benefit_raw = self._get_coupon_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_raw))
        benefit = benefit.get("benefit", benefit)

        notify = benefit.get("notify", False)
        benefit_id = benefit.get("id", None)
        if notify and benefit_id:
            lock_id = self.lock_benefit(
                benefit=benefit,
                pos_id=pos_id,
                applier_sub_type=ApplierSubTypes.COUPON,
            )
            benefit["lockId"] = lock_id

        benefit["applier"] = "Loyalty"
        benefit["applierSubType"] = ApplierSubTypes.COUPON
        return json.dumps(benefit)

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

        params = ("TABLE_INFO", 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: Union[str, int]
        order_id="",        # type: Union[str, int]
        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_info_associated_with_order_picture(
        self,           # type: APIRepository
        pos_id,         # type: int
        order_picture,  # type: str
    ):
        table_info = None
        order_info = OrderFormatter().get_order_picture_dict(order_picture=eTree.fromstring(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_info = self._get_table_info(
                pos_id=pos_id,
                table_id=table_id,
            )
        return table_info

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

        order_info = OrderFormatter().get_order_picture_dict(order_picture=eTree.fromstring(order_picture))
        self.fix_order_info_to_include_api_required_fields(order_info)

        table_info = None
        if table_picture:
            table_info = TableFormatter().get_table_picture_dict(table_picture=eTree.fromstring(table_picture))

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

    @staticmethod
    def fix_order_info_to_include_api_required_fields(order_info):
        order_info["instance"] = 0
        order_info["priceBasis"] = "G"

    def burn_benefit(
        self,                   # type: APIRepository
        benefit_id,             # type: str
        order_picture,          # type: str
        table_picture=None,     # type: Optional[str]
    ):
        # type: (...) -> None

        applier_sub_type = get_applier_sub_type_from_order(
            benefit_id=benefit_id,
            order_picture=order_picture,
            logger=self.logger,
        )

        url = self._get_burn_url(
            benefit_id=benefit_id,
            applier_sub_type=applier_sub_type,
        )
        headers = self._get_headers(applier_sub_type=applier_sub_type)
        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 in [200, 204],
            success=lambda resp: resp,
            failed=lambda resp: errors_list.append((resp.status_code, resp))
        )

        # TODO: Treat burnId in case of 200
        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_coupon_benefit(
        self,               # type: APIRepository
        benefit_id,         # type: str
        order,              # type: Dict
    ):
        # type: (...) -> str

        url = self._get_benefit_url(
            voucher_id=benefit_id,
            applier_sub_type=ApplierSubTypes.COUPON,
        )
        headers = self._get_headers(applier_sub_type=ApplierSubTypes.COUPON)
        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,                # type: Dict[str, Any]
        pos_id,                 # type: int
        applier_sub_type=None,  # type: Optional[str]
        payment_info=None,      # type: Optional[Dict[str, Any]]
    ):
        # type: (...) -> str

        benefit_id = benefit.get("id")
        applier_sub_type = applier_sub_type or benefit.get("applierSubType")

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

        order_picture = self.get_order_picture(pos_id=pos_id)
        table_info = self._get_table_info_associated_with_order_picture(
            pos_id=pos_id,
            order_picture=order_picture,
        )

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

        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 in [200, 204],
            success=lambda resp: resp,
            failed=lambda resp: errors_list.append((resp.status_code, resp))
        )

        if response:
            return self._get_lock_id_from_response(
                response=response,
            )

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

        if benefit:
            applier_sub_type = benefit.get("applierSubType")
        else:
            applier_sub_type = get_applier_sub_type_from_order(
                benefit_id=benefit_id,
                order_picture=order_picture,
                logger=self.logger,
            )
        url = self._get_unlock_url(
            benefit_id=benefit_id,
            applier_sub_type=applier_sub_type,
        )
        headers = self._get_headers(applier_sub_type=applier_sub_type)
        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 in [200, 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_totaled_message(
        self,           # type: APIRepository
        pos_id,         # type: int
        customer_id,    # type: str
    ):
        # type: (...) -> Optional[TotaledMessage]

        try:
            url = self._get_totaled_url(customer_id)
            errors_list = []

            order_picture = self.get_order_picture(
                pos_id=pos_id,
            )
            table_info = self._get_table_info_associated_with_order_picture(
                pos_id=pos_id,
                order_picture=order_picture,
            )

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

            self.logger.debug("Order json sent on get_totaled_message:\n{}".format(body_json))

            response = retry(
                logger=self.logger,
                method_name="get_totaled_message",
                n=self.configs.partner_retry_policy.count,
                sleep_seconds=self.configs.partner_retry_policy.sleep,
                trying_to_do=lambda: requests.request(
                    method="POST",
                    url=url,
                    json=body_json,
                    headers=self._get_headers(applier_sub_type=ApplierSubTypes.LOYALTY),
                    timeout=5,
                ),
                is_success=lambda resp: resp.status_code in (200, 404),
                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:
                if response.status_code == 404:
                    return None
                response_dict = json.loads(response.content)
                translated_message = self._build_translated_message(
                    body_dict=response_dict,
                    message_field="totalizationMessage"
                )
                return TotaledMessage(totalization_message=translated_message)

            response_error = self._get_response_error(errors_list=errors_list)
            raise ResponseError(
                logger=self.logger,
                response=response_error,
            )
        except Exception as ex:
            self.logger.error("Error on get_totaled_message: {}".format(ex))
            raise

    def _get_totaled_url(
        self,           # type: APIRepository
        customer_id,    # type: str
    ):
        # type: (...) -> str

        end_point_url = self.configs.endpoints.get_totaled_message.format(customerId=customer_id)
        full_url = self.configs.credentials[ApplierSubTypes.LOYALTY].base_url + end_point_url

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

        return full_url

    def get_benefit_on_payment(
        self,                   # type: APIRepository
        pos_id,                 # type: int
        loyalty_customer_id,    # type: str
        payment_info,           # type: Dict[str, Union[str, int, float]]
    ):
        # type: (...) -> Optional[Dict]

        url = self._get_payment_url(loyalty_customer_id=loyalty_customer_id)
        if not url:
            raise ServiceNotFoundError()

        headers = self._get_headers(applier_sub_type=ApplierSubTypes.PAYMENT)
        errors_list = []

        order_picture = self.get_order_picture(
            pos_id=pos_id,
        )
        table_info = self._get_table_info_associated_with_order_picture(
            pos_id=pos_id,
            order_picture=order_picture,
        )

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

        response = retry(
            logger=self.logger,
            method_name="payment",
            n=self.configs.partner_retry_policy.count,
            sleep_seconds=self.configs.partner_retry_policy.sleep,
            trying_to_do=lambda: requests.request(
                method="POST",
                url=url,
                json=body_json,
                headers=headers,
                timeout=20,
            ),
            is_success=lambda resp: resp.status_code in (200, 404),
            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:
            if response.status_code == 404:
                return None
            return json.loads(response.content)

        response_error = self._get_response_error(errors_list=errors_list)
        raise ResponseError(
            logger=self.logger,
            response=response_error,
        )

    def end_of_sale_notes(
        self,           # type: APIRepository
        pos_id,         # type: int
        order_id,       # type: int
    ):
        # type: (...) -> Optional[List[Dict[str, str]]]

        url = self._get_end_of_sale_notes_url()
        if not url:
            return None

        headers = self._get_headers(applier_sub_type=ApplierSubTypes.MARKETING)
        errors_list = []

        order_picture = self.get_order_picture(
            pos_id=pos_id,
            order_id=order_id,

        )
        body_json = self._build_order_info(order_picture=order_picture)
        self.logger.info("Order json sent on end of sale:\n{}".format(json.dumps(body_json)))

        response = retry(
            logger=self.logger,
            method_name="end_of_sale_notes",
            n=1,
            sleep_seconds=self.configs.partner_retry_policy.sleep,
            trying_to_do=lambda: requests.request(
                method="POST",
                url=url,
                json=body_json,
                headers=headers,
                timeout=self.configs.credentials[ApplierSubTypes.MARKETING].timeout,
            ),
            is_success=lambda resp: resp.status_code in [200, 204, 404],
            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:
            if response.status_code in [204, 404]:
                return None
            return json.loads(response.content)

        response_error = self._get_response_error(errors_list=errors_list)
        raise ResponseError(
            logger=self.logger,
            response=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.credentials[ApplierSubTypes.LOYALTY].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,               # type: APIRepository
        voucher_id,         # type: str
        applier_sub_type,   # type: str
    ):
        # type: (...) -> str

        end_point_url = self.configs.endpoints.get_benefit.format(voucherId=voucher_id)
        full_url = self.configs.credentials[applier_sub_type].base_url + end_point_url

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

        return full_url

    def _get_end_of_sale_notes_url(
        self,   # type: APIRepository
    ):
        # type: (...) -> Optional[str]

        base_url = self.configs.credentials[ApplierSubTypes.MARKETING].base_url
        if not base_url:
            return None

        end_point_url = self.configs.endpoints.end_of_sale_notes
        full_url = base_url + end_point_url

        return full_url

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

        end_point_url = self.configs.endpoints.burn_benefit.format(benefitId=benefit_id)
        full_url = self.configs.credentials[applier_sub_type].base_url + end_point_url

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

        return full_url

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

        end_point_url = self.configs.endpoints.lock_benefit.format(benefitId=benefit_id)
        full_url = self.configs.credentials[applier_sub_type].base_url + end_point_url

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

        return full_url

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

        end_point_url = self.configs.endpoints.unlock_benefit.format(benefitId=benefit_id)
        full_url = self.configs.credentials[applier_sub_type].base_url + end_point_url

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

        return full_url

    def _get_payment_url(
        self,                   # type: APIRepository
        loyalty_customer_id,    # type: str
    ):
        # type: (...) -> str

        base_url = self.configs.credentials[ApplierSubTypes.PAYMENT].base_url
        if not base_url:
            return ""

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

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

        return full_url

    def _get_headers(
        self,               # type: APIRepository
        applier_sub_type,   # type: str
    ):
        # type: (...) -> Dict

        return {"X-Api-Key": self.configs.credentials[applier_sub_type].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
    ):
        body = response_error.json()
        return ClientCustomError(
            message=self._build_translated_message(
                body_dict=body,
                message_field="message",
            )
        )

    def _build_translated_message(
        self,               # type: APIRepository
        body_dict,          # type: Dict
        message_field,      # type: str
    ):
        # type: (...) -> TranslatedMessage

        try:
            return TranslatedMessage(
                pt=body_dict[message_field].get("pt", ""),
                en=body_dict[message_field].get("en", ""),
                es=body_dict[message_field].get("es", ""),
            )
        except Exception as ex:
            self.logger.exception(msg="Invalid response body from partner - {}".format(body_dict), exc_info=ex)
            return TranslatedMessage(
                pt="",
                en="",
                es="",
            )

    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[int, Response]]
    ):
        # type: (...) -> Optional[Response]

        if not errors_list:
            return None

        for last_error in reversed(errors_list):
            if not isinstance(last_error, Exception):
                _, response = last_error
                return response

        raise errors_list[-1]

    @staticmethod
    def _get_lock_id_from_response(
        response,   # type: Response
    ):
        # type: (...) -> str

        if response.status_code == 200:
            content = json.loads(response.content or "{}")
            return content.get("lockId")

        return ""
