# -*- coding: utf-8 -*-
import json

from threading import Lock
from typing import (
    Any,
    Dict,
    List,
    Optional,
    Union,
)
from xml.etree import cElementTree as eTree

from posot import OrderTaker
from sysactions import get_model, get_posot
from messagebus import (
    Event,
    MessageBus,
)

from application.service import OrderService, BenefitList
from application.validator import BenefitValidator
from application.model import Configurations
from application.model.benefit import ActionResult, Benefit, OrderBenefitInfo
from application.model.conditions import Restrictions
from application.model.transactions import (
    AddBenefitToApplyUnitOfWork,
    StaticUnitOfWork,
    VerifyAndRemoveBenefitsUnitOfWork,
)
from application.model.exceptions import (
    CannotRemoveItemsInATotaledOrder,
    CircularDependencyToApplyDiscountException,
    NeedToRemoveBenefitsPreviousBenefitsException,
    OrderValidationException,
)
from application.repository import NDiscountRepository
from application.util import (
    DictionaryOperations,
    benefit_is_add_item_type_on_total,
    get_applied_benefits_by_key,
    get_benefit_key,
    get_benefit_key_from_persisted_benefits,
    get_benefit_to_apply_from_persisted_benefits,
    get_dict_data_remove,
    get_error_message,
    get_list_data_remove,
    get_new_benefit,
    is_order_taker,
    remove_dict_properties_benefits,
    remove_list_properties_benefits,
    split_benefit_key,
    sum_discounts,
)

from ._ActionService import ActionService


class BenefitService(object):
    def __init__(self, configs, repository, message_bus, order_service):
        # type: (Configurations, NDiscountRepository, MessageBus, OrderService) -> None

        self.configs = configs
        self.repository = repository
        self.message_bus = message_bus
        self.order_service = order_service
        self.logger = configs.logger

        self.product_groups = self.repository.get_all_product_groups()
        self.action_service = ActionService(
            order_service=order_service,
            product_groups=self.product_groups,
            message_bus=self.message_bus,
            logger=self.logger,
        )
        self.automatic_benefits = self._get_benefit_types()

        # TODO: Check if we really need a Lock
        self.discount_lock = {}

    def get_static_unit_of_work(
        self,
        pos_id,                 # type: int
        order_picture=None,     # type: Optional[eTree.Element]
        model=None,             # type: Optional[eTree.Element]
        table_id=None,          # type: Optional[str]
        fill_table_info=False,  # type: Optional[bool]
    ):
        # type: (...) -> StaticUnitOfWork

        if order_picture is None:
            order_picture = self.order_service.get_order_picture(pos_id=pos_id)

        if fill_table_info and table_id is None:
            table_id = order_picture.find(".//CustomOrderProperties/OrderProperty[@key='{}']".format("TABLE_ID"))
            if table_id is not None:
                table_id = table_id.get("value") or None

        static_uow = StaticUnitOfWork(
            pos_id=pos_id,
            order_picture=order_picture,
            model=model,
            table_id=table_id,
        )
        return static_uow

    def get_unit_of_work(
        self,
        pos_id,                 # type: int
        raw_benefit,            # type: Dict
        static_uow=None,        # type: StaticUnitOfWork
        order_picture=None,     # type: Optional[eTree.Element]
        benefit_applier=None,   # type: Optional[str]
        user_auth_level=False,  # type: Optional[bool]
    ):
        # type: (...) -> AddBenefitToApplyUnitOfWork

        benefit = Benefit(raw_benefit=raw_benefit)
        uow = AddBenefitToApplyUnitOfWork(
            logger=self.logger,
            pos_id=pos_id,
            benefit=benefit,
            benefit_applier=benefit_applier,
            order_picture=order_picture,
            order_service=self.order_service,
            repository=self.repository,
            product_groups=self.product_groups,
            static_uow=static_uow,
            user_auth_level=user_auth_level,
        )

        return uow

    def apply_automatic_benefits_on_table(
            self,
            pos_id,         # type: int
            table_id,       # type: str
            is_tab=False    # type: Optional[bool]
    ):
        # type: (...) -> Optional[str]

        if pos_id not in self.discount_lock:
            self.discount_lock[pos_id] = Lock()
        elif self.discount_lock[pos_id].locked():
            return

        with self.discount_lock[pos_id]:
            try:
                if len(self.automatic_benefits) > 0:
                    discount_type = "ON_TAB_OPEN" if is_tab else "ON_TABLE_OPEN"
                    if self.automatic_benefits.get(discount_type) is not None:
                        return self._apply_open_table_benefits(
                            pos_id=pos_id,
                            table_id=table_id,
                            discount_type=discount_type,
                        )
                    else:
                        return

            except Exception as _:
                self.logger.exception("Error applying automatic discounts on POS {}".format(str(pos_id)))
                raise

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

        if pos_id not in self.discount_lock:
            self.discount_lock[pos_id] = Lock()
        elif self.discount_lock[pos_id].locked():
            return

        with self.discount_lock[pos_id]:
            try:
                if len(self.automatic_benefits) > 0:
                    if self.automatic_benefits.get("ON_TAB_OPEN") is not None:
                        return self._apply_tab_benefits(
                            pos_id=pos_id,
                            table_id=table_id
                        )
                    else:
                        return

            except Exception as _:
                self.logger.exception("Error applying automatic discounts on POS {}".format(str(pos_id)))
                raise

    def _get_benefits_to_apply_on_total_by_priority(
        self,
        pos_id,              # type: int
        order_picture=None,  # type: Optional[eTree.Element]
    ):
        # type: (...) -> list

        benefits_to_apply = self._get_benefits_to_apply_on_total(
            pos_id=pos_id,
            order_picture=order_picture,
        )

        benefits_to_apply = sorted(
            benefits_to_apply,
            key=lambda d: (d.get("priority"), not d.get("is_loyalty_benefit", False))
        )

        return benefits_to_apply

    def _get_benefits_to_apply_on_total(
        self,
        pos_id,              # type: int
        order_picture=None,  # type: Optional[eTree.Element]
    ):
        # type: (...) -> list

        order = self.order_service.get_order(
            pos_id=pos_id,
            order_picture=order_picture,
        )

        all_loyalty_benefits = json.loads(s=order.custom_properties.get("LOYALTY_CUSTOMER_BENEFITS", "[]"))
        selected_benefits_ids = json.loads(s=order.custom_properties.get("LOYALTY_COUPONS_TO_APPLY_ON_TOTAL", "[]"))
        benefits_to_apply = self.automatic_benefits.get("ON_TOTAL", [])[:]

        benefits_from_order = self.order_service.get_applied_benefits_from_order_picture(
            pos_id=pos_id,
            order_picture=order_picture,
        )

        applied_benefits_ids = [b["benefit"]["id"] for b in benefits_from_order] if benefits_from_order else []

        if not selected_benefits_ids:
            return benefits_to_apply

        for benefit in all_loyalty_benefits:
            if benefit.get("id") in selected_benefits_ids and benefit.get("id") not in applied_benefits_ids:
                benefit["is_loyalty_benefit"] = True
                benefits_to_apply.append(benefit)

        return benefits_to_apply

    def _apply_automatic_benefits_on_sale(
        self,
        pos_id,         # type: int
        order_picture,  # type: eTree.Element
        static_uow,     # type: StaticUnitOfWork
    ):
        # type: (...) -> eTree.Element

        order_state = order_picture.get("state")
        if order_state != "IN_PROGRESS":
            return order_picture

        benefits_on_sale = self.automatic_benefits.get("ON_SALE") or []
        for benefit in benefits_on_sale:
            updated_order = self._apply_automatic_benefit(
                pos_id=pos_id,
                benefit=benefit,
                static_uow=static_uow,
                order_picture=order_picture,
            )
            order_picture = updated_order if updated_order is not None else order_picture

        return order_picture

    def _apply_automatic_benefits_on_total(
        self,
        pos_id,         # type: int
        order_picture,  # type: eTree.Element
        static_uow,     # type: StaticUnitOfWork
    ):
        # type: (...) -> eTree.Element

        benefits_to_apply = self._get_benefits_to_apply_on_total_by_priority(
            pos_id=pos_id,
            order_picture=order_picture,
        )

        persisted_benefits = self.order_service.get_persisted_benefits_from_order_picture(
            pos_id=pos_id,
            order_picture=order_picture,
        )

        for benefit in benefits_to_apply:
            benefit_key = get_benefit_key_from_persisted_benefits(
                benefit_id=benefit.get("id", ""),
                persisted_benefits=persisted_benefits,
            )
            benefit = get_benefit_to_apply_from_persisted_benefits(
                persisted_benefits=persisted_benefits,
                benefit_key=benefit_key,
                default_benefit=benefit,
            )
            updated_order = self._apply_automatic_benefit(
                pos_id=pos_id,
                benefit=benefit,
                static_uow=static_uow,
                order_picture=order_picture,
                on_total=True,
                persisted_benefit_key=benefit_key,
            )
            order_picture = updated_order if updated_order is not None else order_picture

        return order_picture

    def apply_automatic_benefits_on_order(
        self,
        pos_id,             # type: int
        static_uow=None,    # type: Optional[StaticUnitOfWork]
        on_total=False,     # type: bool
    ):
        # type: (...) -> Optional[eTree.Element]

        if pos_id not in self.discount_lock:
            self.discount_lock[pos_id] = Lock()
        elif self.discount_lock[pos_id].locked():
            return

        with self.discount_lock[pos_id]:
            try:
                if static_uow is None:
                    static_uow = self.get_static_unit_of_work(pos_id=pos_id)

                static_uow.pos_ot.blkopnotify = True
                order_picture = static_uow.order_picture

                if on_total:
                    order_picture = self._apply_automatic_benefits_on_total(
                        pos_id=pos_id,
                        order_picture=order_picture,
                        static_uow=static_uow,
                    )
                else:
                    order_picture = self._apply_automatic_benefits_on_sale(
                        pos_id=pos_id,
                        order_picture=order_picture,
                        static_uow=static_uow,
                    )

            except Exception as _:
                self.logger.exception("Error applying automatic discounts on POS {}".format(str(pos_id)))
                raise

        return order_picture

    def apply_persisted_benefits(
        self,
        pos_id,             # type: int
        static_uow=None,    # type: Optional[StaticUnitOfWork]
    ):
        # type: (...) -> None

        if static_uow is None:
            static_uow = self.get_static_unit_of_work(pos_id=pos_id)

        model = static_uow.model
        if is_order_taker(model=model):
            self.logger.info("The current model is order taker. Ignoring benefit apply for POS: {}".format(pos_id))
            return

        order_picture = static_uow.order_picture
        applied_benefits = self.order_service.get_applied_benefits_from_order_picture(pos_id, order_picture)
        applied_benefits_keys = [b["benefit_key"] for b in applied_benefits]
        benefit_list = self.order_service.get_benefits_from_order_picture(pos_id, order_picture)

        persisted_benefits = benefit_list.get("persistedBenefits", {})
        if not persisted_benefits:
            return

        try:
            pending_benefits = persisted_benefits.get("pending_benefits", {})
            if not pending_benefits:
                return
            for applied_time, benefit_id in persisted_benefits.get("pending_benefits_ids", []):
                benefit_key = "{}_{}".format(benefit_id, applied_time)
                pending_benefit = pending_benefits.get(benefit_key, {})
                is_loyalty_benefit = pending_benefit.get("is_loyalty_benefit")
                if not pending_benefit or benefit_key in applied_benefits_keys or is_loyalty_benefit:
                    return

                uow = self.get_unit_of_work(
                    pos_id=pos_id,
                    raw_benefit=pending_benefit,
                    order_picture=static_uow.order_picture,
                    static_uow=static_uow,
                )
                self.fix_and_apply_benefit(
                    uow=uow,
                    is_manual=True,
                    persisted_key_to_remove=benefit_key,
                )
        except OrderValidationException as _:
            pass
        except Exception as _: # noqa
            exception_message = "Error apply automatic benefit for order {} on POS {}"
            self.logger.exception(exception_message.format(order_picture.get("orderId") or "", str(pos_id)))

    def apply_pending_benefits(
        self,
        pos_id,                             # type: int
        static_uow=None,                    # type: Optional[StaticUnitOfWork]
        previous_error_occurred=False,      # type: Optional[bool]
    ):
        # type: (...) -> None

        if static_uow is None:
            static_uow = self.get_static_unit_of_work(
                pos_id=pos_id
            )

        model = static_uow.model
        if is_order_taker(model=model):
            self.logger.info("The current model is order taker. Ignoring benefit apply for POS: {}".format(pos_id))
            return

        order_picture = static_uow.order_picture
        order_id = order_picture.get("orderId")
        order_benefits = self.order_service.get_applied_benefits_from_order_picture(pos_id, order_picture)

        if not order_benefits:
            self.logger.info("No benefits to orderId: {}; posId: {}".format(order_id, pos_id))
            return

        persisted_benefits = self.order_service.get_persisted_benefits_from_order_picture(
            pos_id=pos_id,
            order_picture=order_picture,
        )

        self.logger.info("Trying to apply benefits to orderId: {}; posId: {}".format(order_id, pos_id))
        for order_benefit_json in order_benefits:
            benefit_order_info = OrderBenefitInfo.from_json(order_benefit_json)
            if benefit_order_info.benefit_applied:
                continue

            self._apply_pending_benefit(
                pos_id=pos_id,
                benefit_order_info=benefit_order_info,
                persisted_benefits=persisted_benefits,
                static_uow=static_uow,
                previous_error_occurred=previous_error_occurred,
            )

    def _send_discount_applied_event(
        self,
        pos_id,    # type: str
        order_id,  # type: str
    ):
        # type: (...) -> None

        try:
            event_data = {
                "posId": pos_id,
                "orderId": order_id,
            }
            event = Event(
                subject="DISCOUNT_APPLIED",
                data=json.dumps(event_data),
            )
            self.message_bus.publish_event(event=event)
            self.logger.info("DISCOUNT_APPLIED event sent successfully! order_id={0}".format(order_id))
        except Exception as _: # noqa
            self.logger.exception("Error sending DISCOUNT_APPLIED event")

    def fix_and_apply_benefit(
        self,
        uow,                                # type: AddBenefitToApplyUnitOfWork
        validate_benefits=True,             # type: Optional[bool]
        on_total=False,                     # type: Optional[bool]
        is_manual=False,                    # type: Optional[bool]
        persisted_key_to_remove='',         # type: Optional[str]
        previous_error_occurred=False,      # type: Optional[bool]
    ):
        # type: (...) -> None

        some_error_occurred = False
        try:
            self._apply_benefit(
                uow=uow,
                validate_benefits=validate_benefits,
                on_total=on_total,
                is_manual=is_manual,
                persisted_key_to_remove=persisted_key_to_remove,
            )
        except NeedToRemoveBenefitsPreviousBenefitsException as ex:
            try:
                some_error_occurred = True

                self.remove_benefits(
                    pos_id=uow.pos_id,
                    benefit_keys=ex.benefits_ids,
                    static_uow=uow.static_uow,
                    delete_from_order=False,
                )

                uow.update_order(order_picture=uow.static_uow.order_picture)

                self._apply_benefit(
                    uow=uow,
                    validate_benefits=validate_benefits,
                    on_total=on_total,
                    is_manual=is_manual,
                    persisted_key_to_remove=persisted_key_to_remove,
                )

                if previous_error_occurred:
                    raise CircularDependencyToApplyDiscountException()
            except NeedToRemoveBenefitsPreviousBenefitsException as _:
                raise CircularDependencyToApplyDiscountException()
            finally:
                if not previous_error_occurred:
                    self.apply_pending_benefits(
                        pos_id=uow.pos_id,
                        static_uow=uow.static_uow,
                        previous_error_occurred=some_error_occurred,
                    )

                    uow.update_order(order_picture=uow.static_uow.order_picture)

    def _apply_benefit(
        self,
        uow,                        # type: AddBenefitToApplyUnitOfWork
        validate_benefits,          # type: Optional[bool]
        on_total,                   # type: Optional[bool]
        is_manual,                  # type: Optional[bool]
        persisted_key_to_remove,    # type: Optional[str]
    ):
        # type: (...) -> None

        try:
            message = "Adding benefit to orderId: {}; posId: {}; benefitName: {}"
            self.logger.info(message.format(uow.order.id, uow.pos_id, uow.benefit.name))

            order_picture = uow.order.order_picture
            if order_picture is None:
                order_picture = self.order_service.get_order_picture(uow.pos_id)

            applied_benefits = self.order_service.get_applied_benefits_from_order_picture(uow.pos_id, order_picture)
            burnt_items = self.order_service.get_burnt_items(applied_benefits)

            if validate_benefits:
                BenefitValidator.validate_benefits(uow, applied_benefits, burnt_items, self.product_groups)

            benefit_key = get_benefit_key(benefit_id=uow.benefit_id)
            uow.set_benefit_key(benefit_key)
            action_result = ActionResult([], {}, [], {}, True, False)
            already_burnt = False

            for action in uow.benefit.actions:
                action.times_to_apply = self._get_benefit_times_to_apply(uow)
                restrictions = Restrictions(uow.pos_id, order_picture, uow.benefit, self.repository, self.order_service)
                result = self.action_service.apply_action(action, burnt_items, uow, uow.pos_ot, restrictions, on_total)
                if result.benefit_ids_to_remove:
                    raise NeedToRemoveBenefitsPreviousBenefitsException(benefits_ids=result.benefit_ids_to_remove)

                action_result += result
                items_to_burn = result.items_to_burn
                if len(items_to_burn) > 0 and not already_burnt:
                    updated_items_to_burn = DictionaryOperations.unmerge_counting_dict(items_to_burn, burnt_items)
                    action_result.items_to_burn = updated_items_to_burn
                    already_burnt = True

            if len(uow.benefit.actions) == 0 or action_result:
                updated_order = self._save_benefits_list(
                    uow=uow,
                    pos_ot=uow.pos_ot,
                    action_result=action_result,
                    persisted_key_to_remove=persisted_key_to_remove,
                )
                uow.update_order(order_picture=updated_order)

                if is_manual and action_result.benefit_applied:
                    self._send_discount_applied_event(
                        pos_id=str(uow.pos_id),
                        order_id=str(uow.order.id),
                    )

                message = "Benefit applied successfully to orderId: {}; posId: {}; benefitName: {};"
                self.logger.info(message.format(uow.order.id, uow.pos_id, uow.benefit.name))
            else:
                message = "Benefit not applied to orderId: {}; posId: {}; benefitName: {};"
                self.logger.info(message.format(uow.order.id, uow.pos_id, uow.benefit.name))
        except OrderValidationException as ex:
            message = "Invalid benefit to orderId: {}; posId: {}; benefitName: {}; validation: {}"
            self.logger.info(message.format(uow.order.id, uow.pos_id, uow.benefit.name, ex.message))
            raise

    @staticmethod
    def _get_benefit_times_to_apply(
        uow,    # type: AddBenefitToApplyUnitOfWork
    ):
        # type: (...) -> int

        return uow.benefit.times_to_apply or (uow.benefit_order_info and uow.benefit_order_info.times_to_apply) or 0

    def validate_benefits(
        self,
        pos_id,                 # type: int
        static_uow,             # type: StaticUnitOfWork
        product_groups=None,    # type: Dict
    ):
        # type: (...) -> eTree

        order_picture = static_uow.order_picture
        if order_picture is None:
            order_picture = self.order_service.get_order_picture(pos_id)

        applied_benefits = self.order_service.get_applied_benefits_from_order_picture(pos_id, order_picture)
        if not applied_benefits:
            return order_picture

        order = self.order_service.get_order(pos_id, order_picture=order_picture)
        uow = VerifyAndRemoveBenefitsUnitOfWork(pos_id, order)

        log_message = "Validating used benefits from orderId: {}; posId: {}"
        self.logger.info(log_message.format(uow.order.id, uow.pos_id))

        burnt_items = DictionaryOperations.merge_counting_dicts([
            applied_benefit.get("burnt_items", {}) for applied_benefit in applied_benefits
        ])

        removed_benefit = False
        for applied_benefit in applied_benefits:
            benefit = Benefit(applied_benefit.get("benefit"))
            benefit_key = applied_benefit.get("benefit_key")
            benefit_burnt_items = applied_benefit.get("burnt_items", {})
            burnt_items_for_benefit = DictionaryOperations.unmerge_counting_dict(burnt_items, benefit_burnt_items)
            valid_benefit = self._validate_benefit(
                benefit_key=benefit_key,
                benefit=benefit,
                uow=uow,
                burnt_items_for_benefit=burnt_items_for_benefit,
                product_groups=product_groups,
                static_uow=static_uow,
            )
            if valid_benefit is False:
                removed_benefit = True

        if removed_benefit:
            order = self.order_service.get_order(pos_id)
            if sum([x.qty for x in order.sale_lines]) == 0:
                self.order_service.void_order(pos_id, uow.pos_ot)

            return order.order_picture

        return order_picture

    def remove_benefits(
        self,
        pos_id,                  # type: int
        benefit_keys,            # type: List
        static_uow=None,         # type: Optional[StaticUnitOfWork]
        delete_from_order=True,  # type: Optional[bool]
    ):
        # type: (...) -> None

        if static_uow is None:
            static_uow = self.get_static_unit_of_work(pos_id=pos_id)
        order_picture = static_uow.order_picture

        order_id = order_picture.get("orderId")
        log_message = "Trying to remove benefits to orderId: {}; posId: {}; benefitIds: {}"
        self.logger.info(log_message.format(order_id, pos_id, benefit_keys))

        if benefit_keys is None:
            self.logger.info("BenefitIds not provided. Removing all order benefits")
            all_benefits = self.order_service.get_applied_benefits_from_order_picture(pos_id, order_picture)
            benefit_keys = [x.get("benefit_key") for x in all_benefits]

        self.remove_benefit(
            pos_id=pos_id,
            benefit_keys=benefit_keys,
            delete_from_order=delete_from_order,
            static_uow=static_uow,
        )

    def remove_benefit(
        self,                    # type: BenefitService
        pos_id,                  # type: int
        benefit_keys,            # type: List[str]
        delete_from_order=True,  # type: Optional[bool]
        static_uow=None,         # type: Optional[StaticUnitOfWork]
    ):
        # type: (...) -> None

        order_picture = static_uow.order_picture
        applied_benefits = self.order_service.get_applied_benefits_from_order_picture(
            pos_id=pos_id,
            order_picture=order_picture,
        )
        if not applied_benefits:
            return

        sorted_applied_benefits = sorted(
            applied_benefits,
            key=lambda b: (b.get("benefit_key").split("_")[-1]),
        )

        benefit_keys_to_remove = get_applied_benefits_by_key(
            benefit_keys=benefit_keys,
            applied_benefits=sorted_applied_benefits,
        )
        self.check_if_benefits_are_removable(
            pos_id=str(pos_id),
            benefits_to_remove=benefit_keys_to_remove,
            order_picture=order_picture,
        )
        benefit_keys_to_remove = [b.get("benefit_key") for b in benefit_keys_to_remove]

        benefits_to_keep = []
        keys_remove = []
        added_sale_lines = []
        used_discount_ids = []
        used_tender_ids = []
        item_amounts_remove = {}

        index = self._get_index_of_first_benefit_to_remove(
            benefit_keys_to_remove=benefit_keys_to_remove,
            sorted_applied_benefits=sorted_applied_benefits,
        )
        benefits_to_keep.extend(sorted_applied_benefits[:index])

        for benefit in sorted_applied_benefits[index:]:
            benefit_key = benefit.get("benefit_key")
            used_discount_ids.extend(benefit.get("used_discount_ids", []))
            item_amounts_remove.update(benefit.get("discount_item_values", {}))
            benefit["used_discount_ids"] = []
            benefit["benefit_applied"] = False

            if benefit_key in benefit_keys_to_remove:
                added_sale_lines.extend(benefit.get("added_sale_lines", []))
                used_tender_ids.extend(benefit.get("order_tenders_ids", []))
                benefit["order_tenders_ids"] = []
                if delete_from_order:
                    keys_remove.append(benefit_key)
                else:
                    benefits_to_keep.append(benefit)

                message = "Remove benefits: Removing benefit. BenefitName: {}; DeleteFromOrder: {}"
                self.logger.info(message.format(benefit["benefit"].get("name"), delete_from_order))
            else:
                if self._benefit_must_be_kept(benefit=benefit):
                    benefit["benefit_applied"] = True

                    message = "Remove benefits: Benefit with added items (not removed). BenefitName: {};"
                    self.logger.info(message.format(benefit["benefit"].get("name")))
                else:
                    used_tender_ids.extend(benefit.get("order_tenders_ids", []))
                    benefit["order_tenders_ids"] = []

                    message = "Remove benefits: Changing to pending. BenefitName: {};"
                    self.logger.info(message.format(benefit["benefit"].get("name")))
                benefits_to_keep.append(benefit)

        pos_ot = static_uow.pos_ot
        self.order_service.void_line(pos_id, pos_ot, added_sale_lines)
        self.order_service.clear_discounts(pos_ot, used_discount_ids)
        self.order_service.clear_tenders(pos_id, pos_ot, used_tender_ids)

        updated_order_picture = self._set_benefits_to_apply(
            pos_id=pos_id,
            benefits=benefits_to_keep,
            pos_ot=pos_ot,
            keys_remove=keys_remove,
            item_amounts_remove=item_amounts_remove,
            tenders_remove=used_discount_ids,
            discounts_remove=used_tender_ids,
            order_picture=order_picture,
        )

        static_uow.set_order_picture(order_picture=updated_order_picture)

    def remove_invalid_benefits(
        self,
        pos_id,         # type: int
        static_uow,     # type: StaticUnitOfWork
    ):
        # type: (...) -> eTree.Element

        order_picture = static_uow.order_picture
        if order_picture is None:
            order_picture = self.order_service.get_order_picture(pos_id)

        applied_benefits = self.order_service.get_applied_benefits_from_order_picture(pos_id, order_picture)
        if not applied_benefits:
            return order_picture

        order = self.order_service.get_order(pos_id, order_picture=order_picture)
        benefits_remove = []
        for applied_benefit in applied_benefits:
            benefit_order_info = OrderBenefitInfo.from_json(applied_benefit)
            if not BenefitValidator.validate_added_sale_line_benefit(benefit_order_info, order):
                benefits_remove.append(benefit_order_info.benefit_key)

        if len(benefits_remove) > 0:
            self.remove_benefits(
                pos_id=pos_id,
                benefit_keys=benefits_remove,
                static_uow=static_uow,
            )

        return order_picture

    def _remove_benefit_by_key(
        self,
        pos_id,               # type: int
        benefit_keys,         # type: List[str]
        keep_as_not_applied,  # type: bool
        static_uow=None,      # type: Optional[StaticUnitOfWork]

    ):
        pass

    @staticmethod
    def _get_index_of_first_benefit_to_remove(
        benefit_keys_to_remove,
        sorted_applied_benefits
    ):
        index = 0
        for benefit in sorted_applied_benefits:
            benefit_key = benefit.get("benefit_key")
            must_be_removed = benefit_key in benefit_keys_to_remove
            if not must_be_removed:
                index += 1
            else:
                break
        return index

    @staticmethod
    def _benefit_must_be_kept(
        benefit,
    ):
        if benefit.get("added_sale_lines"):
            return True

        return False

    def check_if_benefits_are_removable(
        self,
        pos_id,                 # type: str
        benefits_to_remove,     # type: List[Dict]
        order_picture=None,     # type: Optional[eTree.Element]
    ):
        # type: (...) -> None

        if order_picture is None:
            order_picture = self.order_service.get_order_picture(pos_id)

        state = order_picture.get("state")

        if state == u"TOTALED":
            for benefit in benefits_to_remove:
                benefit_action = benefit.get("benefit")
                actions = benefit_action.get("actions")
                for action in actions.values():
                    if action.get("itemsToAdd"):
                        raise CannotRemoveItemsInATotaledOrder()

    def _save_benefits_list(
        self,
        uow,                            # type: AddBenefitToApplyUnitOfWork
        pos_ot,                         # type: OrderTaker
        action_result,                  # type: Optional[ActionResult]
        persisted_key_to_remove='',     # type: Optional[str]
    ):
        # type: (...) -> None

        order_picture = uow.order.order_picture
        old_benefits = self.order_service.get_applied_benefits_from_order_picture(uow.pos_id, order_picture)
        benefit_key = uow.benefit_key

        if uow.benefit_order_info is None:
            order_state = order_picture.get("state")
            str_data = get_new_benefit(
                uow=uow,
                action_result=action_result,
                old_benefits=old_benefits,
                benefit_key=benefit_key,
                order_state=order_state,
            )
        else:
            str_data = self._update_benefit(
                uow=uow,
                action_result=action_result,
                order_picture=order_picture,
                new_benefit_key=benefit_key,
            )

        discount_ids = action_result.discounts_id if not uow.benefit.tip_included else []
        tenders_ids = action_result.order_tenders
        item_values = action_result.discount_item_values
        benefit_list = self._get_benefit_list_to_add(
            uow=uow,
            data=str_data,
            benefit_key=benefit_key,
            item_values=item_values,
            order_tenders_ids=tenders_ids,
            discount_ids=discount_ids,
            persisted_key_to_remove=persisted_key_to_remove,
        )
        updated_order = self.order_service.save_benefit_list_custom_property(pos_ot, benefit_list)
        return updated_order

    def _validate_benefit(
        self,
        benefit_key,                # type: str
        benefit,                    # type: Benefit
        uow,                        # type: VerifyAndRemoveBenefitsUnitOfWork
        burnt_items_for_benefit,    # type: Dict
        product_groups,             # type: Dict
        static_uow,                 # type: StaticUnitOfWork
    ):
        # type: (...) -> bool

        try:
            BenefitValidator.validate_benefit_order_info(benefit, uow.order, burnt_items_for_benefit, product_groups)
            return True
        except OrderValidationException:
            self.remove_benefit(
                pos_id=uow.pos_id,
                benefit_keys=[benefit_key],
                static_uow=static_uow,
            )
            return False

    def _update_benefit(
        self,
        uow,                # type: AddBenefitToApplyUnitOfWork
        action_result,      # type: ActionResult
        order_picture,      # type: eTree.Element
        new_benefit_key,    # type: str
    ):
        # type: (...) -> str

        old_benefits = self.order_service.get_benefits_from_order_picture(
            pos_id=uow.pos_id,
            order_picture=order_picture,
        )
        benefit_list = json.loads(old_benefits.get("benefitList", "[]"))
        list_benefit = []
        last_benefit_key = uow.benefit_order_info.benefit_key
        for benefit in benefit_list:
            if benefit["benefit_key"] == last_benefit_key:
                benefit["benefit_key"] = new_benefit_key
                benefit["discount_item_values"] = action_result.discount_item_values
                benefit["used_discount_ids"] = action_result.discounts_id
                benefit["order_tenders_ids"] = action_result.order_tenders
                benefit["benefit_applied"] = action_result.benefit_applied
                if not benefit["benefit_total_value"]:
                    benefit["benefit_total_value"] = sum_discounts(
                        promotion_tender=uow.promotion_tenders,
                        discount_item_values=action_result.discount_item_values,
                    )

            list_benefit.append(benefit)
            list_benefit = sorted(
                list_benefit,
                key=lambda b: (b.get("benefit_key").split("_")[-1]),
            )
        return json.dumps(list_benefit)

    def _set_benefits_to_apply(
        self,
        pos_id,                 # type: int
        benefits,               # type: List[str]
        pos_ot,                 # type: OrderTaker
        keys_remove,            # type: List[str]
        item_amounts_remove,    # type: Dict
        tenders_remove,         # type: List[int]
        discounts_remove,       # type: List[int]
        order_picture=None,     # type: Optional[eTree.Element]
    ):
        # type: (...) -> Optional[eTree.Element]

        sorted_benefits = sorted(
            benefits,
            key=lambda b: (b.get("benefit_key").split("_")[-1]),
        )
        data = json.dumps(sorted_benefits)
        benefit_list = self._get_benefit_list_to_remove(
            pos_id=pos_id,
            data=data,
            benefit_keys_to_remove=keys_remove,
            item_values=item_amounts_remove,
            order_tenders_ids=tenders_remove,
            discounts_remove=discounts_remove,
            order_picture=order_picture,
        )
        if not benefits and not benefit_list.persisted_benefits:
            benefit_list = "clear"

        return self.order_service.save_benefit_list_custom_property(pos_ot, benefit_list)

    def _get_benefit_types(self):
        response = {}
        automatic_benefits = self.repository.get_all_automatic_benefits()
        for benefit in automatic_benefits:
            benefit_json = json.loads(benefit)
            benefit = benefit_json.get("benefit")
            event = benefit.get("automatic")

            if event not in response:
                response[event] = []

            response[event].append(benefit)

        return response

    def _apply_automatic_benefit(
        self,
        pos_id,                         # type: int
        benefit,                        # type: Dict
        static_uow,                     # type: StaticUnitOfWork
        order_picture=None,             # type: Optional[eTree.Element]
        on_total=False,                 # type: Optional[bool]
        persisted_benefit_key=None,     # type: Optional[str]
    ):
        # type: (...) -> Optional[eTree.Element]

        order_id = None
        uow = None
        try:
            uow = self.get_unit_of_work(
                pos_id=pos_id,
                raw_benefit=benefit,
                static_uow=static_uow,
                order_picture=order_picture,
            )
            order_id = uow.order.id
            self.fix_and_apply_benefit(
                uow=uow,
                on_total=on_total,
                persisted_key_to_remove=persisted_benefit_key,
            )
            return uow.order.order_picture
        except OrderValidationException as _:
            pass
        except Exception as _:
            self.logger.exception(get_error_message(
                order_id=order_id,
                benefit=uow.benefit,
            ))

    def _apply_pending_benefit(
        self,
        pos_id,                             # type: int
        benefit_order_info,                 # type: OrderBenefitInfo
        persisted_benefits,                 # type: Dict
        static_uow,                         # type: StaticUnitOfWork
        previous_error_occurred=False,      # type: Optional[bool]
    ):
        # type: (...) -> None

        order_id = None
        benefit = None
        try:
            benefit = benefit_order_info.benefit

            benefit_raw = get_benefit_to_apply_from_persisted_benefits(
                persisted_benefits=persisted_benefits,
                benefit_key=benefit_order_info.benefit_key,
                default_benefit=benefit.raw,
            )

            uow = self.get_unit_of_work(
                pos_id=pos_id,
                raw_benefit=benefit_raw,
                static_uow=static_uow,
                order_picture=static_uow.order_picture,
            )
            uow.benefit_order_info = benefit_order_info
            order_id = uow.order.id
            self.fix_and_apply_benefit(
                uow=uow,
                validate_benefits=False,
                persisted_key_to_remove=benefit_order_info.benefit_key or "",
                previous_error_occurred=previous_error_occurred,
            )
        except OrderValidationException as _:
            pass
        except CircularDependencyToApplyDiscountException as _:
            raise
        except Exception as _:
            self.logger.exception(get_error_message(
                order_id=order_id,
                benefit=benefit,
            ))

    def _get_benefit_list_to_add(
        self,
        uow,                        # type: AddBenefitToApplyUnitOfWork
        data,                       # type: str
        benefit_key,                # type: Any[str, List[str]]
        item_values,                # type: Dict
        order_tenders_ids,          # type: List
        discount_ids,               # type: List
        persisted_key_to_remove,    # type: str
    ):
        # type: (...) -> BenefitList

        order_picture = uow.order.order_picture
        benefits_pre_total = self._get_benefits_pre_total(
            pos_id=uow.pos_id,
            benefit_key_to_add=benefit_key,
            order_picture=order_picture,
        )
        persisted_benefits_on_total = self._get_persisted_benefits(
            pos_id=uow.pos_id,
            order_picture=order_picture,
            persisted_key_to_remove=persisted_key_to_remove,
        )
        discount_item_values = self._get_discount_item_values(uow.pos_id, item_values, order_picture)
        discount_excluded = self._get_discount_type_excluded(uow.pos_id, discount_ids, order_picture)
        tender_excluded = self._get_tender_type_excluded(uow.pos_id, order_tenders_ids, order_picture)

        return BenefitList(
            benefit_pre_total_sequence=benefits_pre_total,
            persisted_benefits=persisted_benefits_on_total,
            discount_item_values=discount_item_values,
            discount_id_tip_excluded=discount_excluded,
            tender_id_tip_excluded=tender_excluded,
            benefits=data,
        )

    def _get_benefit_list_to_remove(
        self,
        pos_id,                     # type: int
        data,                       # type: str
        benefit_keys_to_remove,     # type: Any[str, List[str]]
        item_values,                # type: Dict
        order_tenders_ids,          # type: List[int]
        discounts_remove,           # type: List[int]
        order_picture=None,         # type: Optional[eTree.Element]
    ):
        # type: (...) -> BenefitList

        if order_picture is None:
            order_picture = self.order_service.get_order_picture(pos_id)

        benefits_pre_total = self._get_benefits_pre_total(
            pos_id=pos_id,
            benefits_keys_to_remove=benefit_keys_to_remove,
            order_picture=order_picture,
        )
        persisted_benefits_on_total = self._get_persisted_benefits(
            pos_id=pos_id,
            order_picture=order_picture,
        )
        discount_item_values = self._remove_properties(pos_id, item_values, "discountItemValues", order_picture, True)
        discount_excluded = self._remove_properties(pos_id, discounts_remove, "discountIdTipExcluded", order_picture)
        tender_excluded = self._remove_properties(pos_id, order_tenders_ids, "tenderIdTipExcluded", order_picture)

        return BenefitList(
            benefit_pre_total_sequence=benefits_pre_total,
            persisted_benefits=persisted_benefits_on_total,
            discount_item_values=discount_item_values,
            discount_id_tip_excluded=discount_excluded,
            tender_id_tip_excluded=tender_excluded,
            benefits=data,
        )

    def _get_benefits_pre_total(
        self,
        pos_id,                         # type: int
        benefit_key_to_add=None,        # type: Any[Union[str, List[str]]]
        benefits_keys_to_remove=None,   # type: List[str]
        order_picture=None,             # type: Optional[eTree.Element]
    ):
        # type: (...) -> List

        benefits = self.order_service.get_benefits_from_order_picture(pos_id, order_picture)
        if order_picture.get("state") == "TOTALED":
            return benefits.get("benefitPreTotalSequence", [])

        if benefits_keys_to_remove:
            return remove_list_properties_benefits(
                benefits=benefits,
                list_remove=benefits_keys_to_remove,
                property_name="benefitPreTotalSequence",
            )

        pre_total_sequence = benefits.get("benefitPreTotalSequence", [])
        if benefit_key_to_add:
            pre_total_sequence.append(benefit_key_to_add)
        return pre_total_sequence

    def _get_persisted_benefits(
        self,
        pos_id,                         # type: int
        order_picture=None,             # type: Optional[eTree.Element]
        persisted_key_to_remove='',     # type: Optional[str]
    ):
        # type: (...) -> Dict

        benefits = self.order_service.get_benefits_from_order_picture(
            pos_id=pos_id,
            order_picture=order_picture,
        )
        persisted_benefits = benefits.get("persistedBenefits", {})
        if not persisted_benefits or not persisted_key_to_remove:
            return persisted_benefits

        benefit_id, applied_time = split_benefit_key(benefit_key=persisted_key_to_remove)
        persisted_benefits["pending_benefits_ids"].remove([applied_time, benefit_id])
        persisted_benefits["pending_benefits"].pop(persisted_key_to_remove)

        if not persisted_benefits["pending_benefits_ids"]:
            return {}

        return persisted_benefits

    def _get_discount_item_values(
        self,
        pos_id,                 # type: int
        item_discount_values,   # type: Dict
        order_picture=None,     # type: Optional[eTree.Element]
    ):
        # type: (...) -> Dict

        benefits = self.order_service.get_benefits_from_order_picture(pos_id, order_picture)
        return get_dict_data_remove(
            benefits=benefits,
            dict_data=item_discount_values,
            property_name="discountItemValues",
        )

    def _get_discount_type_excluded(
        self,
        pos_id,                 # type: int
        discount_ids,           # type: List[str]
        order_picture=None,     # type: Optional[eTree.Element]
    ):
        # type: (...) -> List

        benefits = self.order_service.get_benefits_from_order_picture(pos_id, order_picture)
        return get_list_data_remove(
            benefits=benefits,
            list_data=discount_ids,
            property_name="discountIdTipExcluded",
        )

    def _get_tender_type_excluded(
        self,
        pos_id,                 # type: int
        order_tenders_ids,      # type: List[str]
        order_picture=None,     # type: Optional[eTree.Element]
    ):
        # type: (...) -> List

        benefits = self.order_service.get_benefits_from_order_picture(pos_id, order_picture)
        if len(benefits) == 0 or len(order_tenders_ids) == 0:
            return order_tenders_ids

        return get_list_data_remove(
            benefits=benefits,
            list_data=order_tenders_ids,
            property_name="tenderIdTipExcluded",
        )

    def _remove_properties(
        self,
        pos_id,                 # type: int
        data,                   # type: Union[Dict, List]
        property_name,          # type: str
        order_picture,          # type: eTree.Element
        is_dict_data=False,     # type: Optional[bool]
    ):
        # type: (...) -> Union[Dict, List]

        benefits = self.order_service.get_benefits_from_order_picture(pos_id, order_picture)

        if is_dict_data:
            return remove_dict_properties_benefits(
                benefits=benefits,
                dict_remove=data,
                property_name=property_name,
            )
        return remove_list_properties_benefits(
            benefits=benefits,
            list_remove=data,
            property_name=property_name,
        )

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

        static_uow = self.get_static_unit_of_work(
            pos_id=pos_id,
            table_id=table_id,
        )
        static_uow.pos_ot.blkopnotify = True
        order_picture = static_uow.order_picture

        for benefit in self.automatic_benefits[discount_type]:
            updated_order = self._apply_automatic_benefit(
                pos_id=pos_id,
                benefit=benefit,
                static_uow=static_uow,
                order_picture=order_picture,
            )
            order_picture = updated_order if updated_order is not None else order_picture

        order = self.order_service.get_order(
            pos_id=pos_id,
            order_picture=order_picture,
        )

        order_id = order.id if order else ''
        if sum([x.qty for x in order.sale_lines]) == 0:
            model = get_model(pos_id)
            pos_ot = get_posot(model)
            self.order_service.void_order(pos_id, pos_ot)
            return
        return str(order_id)

    def save_persistent_benefits(
        self,
        pos_id,         # type: int
        order_picture,  # type: eTree.Element
    ):
        # type: (...) -> eTree.Element

        benefit_list = self.order_service.get_benefits_from_order_picture(
            pos_id=pos_id,
            order_picture=order_picture,
        )
        if not benefit_list:
            return order_picture

        all_benefits = json.loads(benefit_list.get("benefitList", "[]"))
        applied_discounts = [x for x in all_benefits if x['benefit_applied'] is True]
        if not applied_discounts:
            return order_picture

        persisted_benefits = dict(
            pending_benefits_ids=[],
            pending_benefits={},
        )
        for discount in applied_discounts:
            benefit = discount.get("benefit", {})
            persist = benefit.get("persistent", None)
            if not persist:
                continue

            if benefit_is_add_item_type_on_total(benefit=benefit):
                continue

            if persist == "KEEP_VALUE" and "persistentValue" not in benefit.keys():
                benefit["persistentValue"] = discount.get("benefit_total_value", 0)

            benefit_key = discount.get("benefit_key")
            benefit_id, applied_time = split_benefit_key(benefit_key=benefit_key)
            persisted_benefits["pending_benefits_ids"].append([applied_time, benefit_id])
            persisted_benefits["pending_benefits"].update({benefit_key: benefit})

        persisted_benefits["pending_benefits_ids"].sort()

        benefit_list["persistedBenefits"] = persisted_benefits

        if persisted_benefits["pending_benefits_ids"]:
            pos_ot = get_posot(model=get_model(posid=pos_id))
            self.order_service.save_benefit_list_custom_property(
                pos_ot=pos_ot,
                benefit_list=benefit_list,
            )
            return self.order_service.get_order_picture(pos_id=pos_id)

        return order_picture

    @staticmethod
    def send_order_update_event(
        static_uow,     # type: StaticUnitOfWork
    ):
        # type: (...) -> None

        static_uow.pos_ot.blkopnotify = False
        pos_ot = static_uow.pos_ot
        sale_type = static_uow.order_picture.get("saleType")
        pos_ot.updateOrderProperties(
            posid=static_uow.pos_id,
            saletype=sale_type,
        )
