import json
import requests

from requests import Response
from typing import Optional, List, Tuple

from helper import retry

from application.model.configuration import Configurations
from application.model.customexception import NotFound, ForbiddenError, ProcessorError, InUseError, \
    NotAvailableError, NotFoundError, NoResponseError, AlreadyUnlockedError
from application.model.orderinfo import Order, OrderJsonEncoder


class APIRepository(object):

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

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

    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()
            errors_list = []

            response = retry(
                logger=self.logger,
                method_name="retrieve_loyalty_customer_info",
                n=1,
                sleep_seconds=1,
                trying_to_do=lambda: requests.request("GET", url=url, headers=headers, timeout=20),
                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, voucher_id):
        # type: (int, str) -> Optional[str, bool]

        order_info = self._get_order_info()

        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_order_info(self):
        order = Order(self.configs.store_id)
        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,          # Dict
    ):
        # 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 == 406:
            raise NotAvailableError(self.logger, response_error)
        if status_code == 409:
            raise InUseError(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 == 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
