"""Services to connect with service."""
import math
from elasticsearch_dsl import Search, Q
from . import EsClient, SERVICE_CACHE_INDEX
from .exceptions import APIException


class ServiceBase(object):
    def __init__(self, service_id, units, raise_exception=False):
        self.service_id = service_id
        self.units = units or 1
        self._total_price = 0.0
        self._total_commission = 0.0
        self._total_discount = 0.0
        self._tax_amount = 0.0
        self._coupon_code = None
        self._service_details = None
        self.is_valid_service = False

        index_query = Search(
            using=EsClient, index=SERVICE_CACHE_INDEX
        ).query("match", id=self.service_id)

        response = index_query.execute()
        if len(response.hits) < 1:
            if raise_exception:
                raise APIException("Service not found", 404)
        else:
            self._service_details = response.hits[0]
            self.is_valid_service = True

    @property
    def total_commission(self):
        return self._total_commission

    @property
    def service_details(self):
        if not self._service_details:
            raise APIException("Service details not found")
        return self._service_details

    @property
    def data(self):
        return self.service_details.to_dict()

    @property
    def total_discount(self):
        return self._total_discount

    @property
    def coupon_code(self):
        return self._coupon_code

    @property
    def tax_details(self):
        return self.service_details.tax.to_dict()

    @property
    def total_price(self):
        return self._total_price


class PriceCatalog(ServiceBase):
    """
    Service price class to fetch service fee structure from `Elasticsearch` index.
    service_id: Respective service id,
    units: Units/quantity of service
    -----------------------------------------------------------------------------
    Call `ServicePrice(service_id, units).get_fee_details()` to get service fee details.

    """
    FLAT = 'FLAT'
    TIER = 'TIER'

    def get_fee_details(self, include_tax=False):
        """
        Return Service fee details based on service fee structure (FLAT/TIER).
        """
        response = dict()
        units = self.units
        obj = self.service_details
        response['service_name'] = obj.name
        if obj.fee_structure == ServicePrice.FLAT:

            if not obj.service_fee_detail:
                raise APIException(
                    'Flat service fee structure pricing not available'
                )

            flat = obj.service_fee_detail[0]
            self._total_price = units * flat.total_cost
            response['hard_cost'] = flat.hard_cost
            response['fees'] = flat.fees
            response['set_up_cost'] = flat.set_up_cost
            response['is_hard_cost_pay_by_dcg'] = flat.is_hard_cost_pay_by_dcg
            response['total_price'] = self._total_price
            response['total_commission'] = self.get_flat_commission(self._total_price)
            response['amount_break_down'] = [flat.to_dict()]

        elif obj.fee_structure == ServicePrice.TIER:
            tiers = obj.service_tier_fee_detail
            tiers = list(filter(lambda k: k.unit_from <= units, tiers))
            tiers = sorted(tiers, key=lambda k: k.unit_from, reverse=True)

            if not tiers:
                raise APIException(
                    'Tier service fee structure pricing not available'
                )

            # Removed as per client requirement
            # if tiers[0].unit_to and tiers[0].unit_to < units:
            #     raise APIException(
            #         'You can take max of ' + str(tiers[0].unit_to) + ' units'
            #     )

            total_price, total_commission = self.tier_cal_generator(tiers)

            response['total_price'] = total_price
            response['total_commission'] = total_commission
            response['amount_break_down'] = [tier.to_dict() for tier in tiers]

        service_tax = obj.tax
        if service_tax.tax_percentage:
            self._tax_amount = self.get_service_tax(self._total_price)
            if include_tax:
                self._total_price += self._tax_amount

        response['total_price'] = self._total_price
        response['tax_amount'] = self._tax_amount
        response['tax_code'] = service_tax.tax_code
        response['tax_percentage'] = service_tax.tax_percentage

        return response

    def get_service_tax(self, total_price):
        service_tax = self.service_details.tax
        tax_amount = self.percentage(total_price, service_tax.tax_percentage)
        return tax_amount

    def tier_cal_generator(self, tiers):
        """
        Return/calculate tier based service price and commission.
        """
        units = self.units
        self._total_price = 0.0
        self._total_commission = 0.0

        for obj in tiers:
            if obj.price_type == "GROUP":
                group_of = units - (obj.unit_from - 1)
                obj.total_price = obj.price * (math.ceil(group_of / obj.group_by))
                self._total_commission += self.get_tier_commission(obj.total_price, obj)
                self._total_price += obj.total_price
                continue

            if obj.price_type == "TOTAL":
                obj.total_price = obj.price
                self._total_commission += self.get_tier_commission(obj.total_price, obj)
                self._total_price += obj.total_price
                continue

            if not obj.unit_to:
                unit = units - (obj.unit_from - 1)
            else:
                if obj.unit_to and (obj.unit_from <= units <= obj.unit_to):
                    unit = units - (obj.unit_from - 1)
                else:
                    unit = obj.unit_to - (obj.unit_from - 1)

            obj.total_price = unit * obj.price
            self._total_commission += self.get_tier_commission(obj.total_price, obj)
            self._total_price += obj.total_price
        return self._total_price, self._total_commission

    @staticmethod
    def percentage(part, whole):
        return (float(part) * float(whole)) / 100

    def get_flat_commission(self, service_price):
        """
        If Service is flat calculate commission from ServiceCommissions.
        If The commission_type is Flat return commission as figure value.
        If The commission_type is PERCENTAGE Calculate commission
        by ((service_total_price*commission.figure_value)/100).
        """
        total_commission = 0.0
        service = self.service_details
        if service.fee_structure == 'FLAT':
            if not service.service_commissions:
                return total_commission

            commission_obj = service.service_commissions[0]

            if commission_obj.commission_type == "FLAT":
                total_commission = commission_obj.figure_value
            else:
                total_commission = self.percentage(service_price, commission_obj.figure_value)

        self._total_commission = total_commission

        return total_commission

    def get_tier_commission(self, service_price, service_tier):
        """
        If Service is tier calculate commission from ServiceTierCommissions.
        If The commission_type is Flat return commission as figure value.
        If The commission_type is PERCENTAGE Calculate commission
        by ((service_total_price*commission.figure_value)/100).
        """
        total_commission = 0.0
        service = self.service_details

        if not service.service_tier_commissions:
            return total_commission

        commission_obj = list(
            filter(
                lambda k: k.service_tier_fee_detail.id == service_tier.id,
                service.service_tier_commissions
            )
        )

        if not commission_obj:
            return total_commission

        commission_obj = commission_obj[0]

        if commission_obj.commission_type == "FLAT":
            total_commission = commission_obj.figure_value
        else:
            total_commission = self.percentage(service_price, commission_obj.figure_value)

        return total_commission


class CouponMixin(ServiceBase):
    """
    Coupon mixin class contains coupon related functions and validator.
    Add function and methods according to your need.
    """

    @classmethod
    def get_coupon_by_service(cls, service_id, coupon_code, to_dict=False):
        """
        Return elastic search dsl object of coupon or None.
        to_dict: True will return dict obj of coupon.
        """
        coupon_obj = None
        if service_id and coupon_code:
            result = Search(
                using=EsClient,
                index=SERVICE_CACHE_INDEX
            ).query('nested',
                    path='coupons',
                    query=Q('match',
                            coupons__code=coupon_code)).filter(
                Q('bool',
                  should=Q('match', id=service_id), minimum_should_match=1)).execute()
            if result and result.hits:
                coupon_obj = result.hits[0]
                if to_dict:
                    coupon_obj = coupon_obj.to_dict()

        return coupon_obj

    def get_coupon_from_service(self, coupon_code, service=None):
        """
        Returns coupon from service coupons list.
        """
        if not service:
            service = self.service_details

        coupon = list(
            filter(
                lambda coup: coup.code == coupon_code, service.coupons
            )
        )
        if coupon:
            return coupon[0]

        return None

    def get_coupon_details(self, coupon_code=None, **kwargs):
        """
        Fetch respective coupon from the service coupon index,
        Validate coupon price and apply on service total price.

        pass get_coupon_details(service_list=list(service_list))
        Service list is required to validate coupon of dependent services,
        If coupon is dependent on other service then we need to check that service
        is added in service list.
        """
        total_price = self.total_price
        coupons_to_apply = list()

        # service_list to validate dependent service condition
        service_list = kwargs.get('service_list') or list()
        coupon_code_list = kwargs.get('coupon_code_list') or list()
        service = self.service_details
        service_coupons = service.coupons or list()

        if coupon_code:
            coupon_code_list.append(coupon_code)

        for c_code in coupon_code_list:
            coupons_to_apply += list(
                filter(
                    lambda coup: coup.code == c_code, service_coupons
                )
            )

        for coupon in service_coupons:
            if coupon.is_auto_apply:
                coupons_to_apply.append(coupon)

        coupons_to_apply = list(
            filter(
                lambda coupon_obj: coupon_obj.min_amount_for_coupon < total_price, coupons_to_apply
            )
        )

        if coupons_to_apply:
            coupon_to_be_apply = self._get_coupon_with_max_discount(coupons_to_apply)
            if coupon_to_be_apply.dependent_services:
                is_valid_coupon = any(
                    dependent.service_id in service_list for dependent in coupon_to_be_apply.dependent_services
                )
                if not is_valid_coupon:
                    coupons_to_apply = self._remove_coupon_from_list(
                        coupon_code=coupon_to_be_apply.code,
                        coupon_list=coupons_to_apply
                    )
                    coupon_to_be_apply = self._get_coupon_with_max_discount(coupons_to_apply)

            if coupon_to_be_apply:
                self._total_discount = coupon_to_be_apply.max_discount
                self._coupon_code = coupon_to_be_apply.code

        return self._total_discount, self._coupon_code

    @classmethod
    def _get_coupon_with_max_discount(cls, coupon_list):
        if coupon_list:
            return max(coupon_list, key=lambda k: k.max_discount)

    @classmethod
    def _remove_coupon_from_list(cls, coupon_code, coupon_list):
        coupons_to_apply = []

        for coupon in coupon_list:
            if coupon.code != coupon_code:
                coupons_to_apply.append(coupon)

        return coupons_to_apply

    def validate_coupon_code(self, coupon_code: str, service_list=None, raise_exception=False, **kwargs):
        """
        Validates the coupon is valid with respect to service_list passed.
        Service list is required to validate coupon of dependent services,
        If coupon is dependent on other service then we need to check that service
        is added in service list.
        """
        is_valid_coupon = False
        service = self.service_details

        if not service_list:
            service_list = list()

        if service_list and kwargs.get('re_fetch'):
            coupon_search_query = Search(
                using=EsClient, index=SERVICE_CACHE_INDEX
            ).query('nested', path='coupons', query=Q('match', coupons__code=coupon_code)).filter(
                Q('bool',
                  should=[Q('match', id=service_id) for service_id in service_list],
                  minimum_should_match=1)
            )
            response = coupon_search_query.execute()
            if response and response.hits:
                is_valid_coupon = True
                service = response.hits[0]

        coupon = self.get_coupon_from_service(coupon_code, service)

        if coupon:
            if coupon.dependent_services:
                # Service list is required to validate coupon of dependent services,
                # If coupon is dependent on other service then we need to check that service
                # is added in service list.
                service_list = service_list or []
                is_valid_coupon = any(dependent.service_id in service_list for dependent in coupon.dependent_services)
            else:
                is_valid_coupon = True

        if not is_valid_coupon and raise_exception:
            raise APIException({'coupon_code': ['Invalid coupon code']})

        return is_valid_coupon


class ServicePrice(PriceCatalog, CouponMixin):
    """
    Service price class to fetch service fee structure from `Elasticsearch` index.
    service_id: Respective service id,
    units: Units/quantity of service
    -----------------------------------------------------------------------------
    Call `ServicePrice(service_id, units).get_fee_details()` to get service fee details.

    Add more functions & calculations acc. to your need.
    """

    pass


class ServiceValidator(object):
    """
    Validate service or service list passed to validator,
    search and validate service using service_id from `Elasticsearch`.
    """
    def __init__(self, service_id=None, raise_exception=False, **kwargs):
        """
        kwargs service_list: validates list of services.
        kwargs match_all: validates all services else validate at least one service.
        """
        self.is_valid_service = False
        self.service_list = kwargs.get('service_list') or list()
        self.minimum_should_match = 1
        if service_id:
            self.service_list.append(service_id)
        if kwargs.get('match_all'):
            self.minimum_should_match = len(self.service_list)

        self.is_valid_service = self.validate_service_list()

        if not self.is_valid_service and raise_exception:
            raise APIException("Invalid service")

    def validate_service_list(self):
        """
        Search and return bool of service exists in Elasticsearch or not.
        """
        result = Search(
            using=EsClient,
            index=SERVICE_CACHE_INDEX
        ).query(
            Q('bool',
              should=[Q('match', id=service_id) for service_id in self.service_list],
              minimum_should_match=self.minimum_should_match)
        ).execute()
        if result and result.hits:
            return True
        return False
