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

from posot import OrderTaker
from typing import Dict, Optional, List, Any
from threading import Lock
from xml.etree.ElementTree import Element
from xml.etree import cElementTree as eTree

from sysactions import get_model, get_posot
from messagebus import MessageBus

from application.service import OrderService, BenefitList
from application.util import DictionaryOperations
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, VerifyAndRemoveBenefitsUnitOfWork
from application.model.exceptions import (
    OrderValidationException,
    BenefitNotFound,
    InvalidMessageContentException,
    DiscountNotFound,
    CannotRemoveItemsInATotaledOrder,
)
from application.repository import NDiscountRepository

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, self.product_groups, self.logger)
        self.automatic_benefits = self._get_benefit_types()

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

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

        benefit = Benefit(raw_benefit)
        uow = AddBenefitToApplyUnitOfWork(
            self.logger,
            pos_id,
            benefit,
            benefit_applier,
            tender_id,
            order_picture,
            self.order_service,
            self.repository,
            self.product_groups,
            table_id=table_id,
            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:
                discount_type = "ON_TAB_OPEN" if is_tab else "ON_TABLE_OPEN"
                if len(self.automatic_benefits) > 0:
                    if self.automatic_benefits.get(discount_type) is not None:
                        return self._apply_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 apply_automatic_benefits_on_order(self, pos_id, on_total=False):
        # type: (int, bool) -> Optional[eTree]

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

        order_picture = None
        with self.discount_lock[pos_id]:
            try:
                if len(self.automatic_benefits) > 0:
                    order_picture = self.order_service.get_order_picture(pos_id)
                    order_state = order_picture.get("state")

                    if not on_total:
                        if order_state == "IN_PROGRESS" and self.automatic_benefits.get("ON_SALE") is not None:
                            for benefit in self.automatic_benefits["ON_SALE"]:
                                updated_order = self._apply_automatic_benefit(pos_id, benefit, order_picture)
                                order_picture = updated_order if updated_order is not None else order_picture
                    elif self.automatic_benefits.get("ON_TOTAL") is not None:
                        for benefit in self.automatic_benefits["ON_TOTAL"]:
                            updated_order = self._apply_automatic_benefit(pos_id, benefit, order_picture, on_total)
                            order_picture = updated_order if updated_order is not None else order_picture

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

        return order_picture

    def apply_manual_pending_benefits(self, pos_id, order_picture=None):
        # type: (int, Optional[eTree]) -> None

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

        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_benefit_list_from_order_picture(pos_id, order_picture)

        if "manualBenefitOnTotalSequence" not in benefit_list:
            return

        try:
            manual_benefits = benefit_list["manualBenefitOnTotalSequence"] or []
            for item in manual_benefits:
                key, benefit = item.items()[0]
                if key not in applied_benefits_keys:
                    uow = self.get_unit_of_work(pos_id, benefit)
                    self.apply_benefit(uow, is_manual=True, remove_manual_key=key)
        except OrderValidationException as _:
            pass
        except Exception as _:
            exception_message = "Error apply automatic benefit for order {} on POS {}"
            self.logger.exception(exception_message.format(order_picture.get("orderId", ""), str(pos_id)))

    def apply_pending_benefits(self, pos_id, order_picture=None):
        # type: (int, Optional[eTree]) -> None

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

        if order_picture is None:
            order_picture = self.order_service.get_order_picture(pos_id)
        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))

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

    def apply_benefit(self, uow, validate_benefits=True, on_total=False, is_manual=False, remove_manual_key=''):
        # type: (AddBenefitToApplyUnitOfWork, bool, bool, bool, str) -> 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 = self._get_benefit_key(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 =\
                    uow.benefit.times_to_apply or (uow.benefit_order_info and uow.benefit_order_info.times_to_apply)
                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)
                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 action_result:
                self._save_benefits_list(uow, uow.pos_ot, action_result, is_manual, remove_manual_key)
                uow.update_order()

                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

    def validate_benefits(self, pos_id, order_picture=None, product_groups=None):
        # type: (int, Optional[eTree], Dict) -> eTree

        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, uow, burnt_items_for_benefit, product_groups)
            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, benefit_keys, order_picture=None, delete_manual=True):
        # type: (int, List, Optional[eTree], bool) -> None

        model = get_model(pos_id)
        pos_ot = get_posot(model)
        if order_picture is None:
            order_picture = self.order_service.get_order_picture(pos_id)

        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_ot, benefit_keys, order_picture, delete_manual)

    def remove_benefit(self, pos_id, pos_ot, benefit_keys, order_picture=None, delete_manual=True):
        # type: (int, eTree, List[str], Optional[eTree], Optional[bool]) -> None

        self._remove_benefit_by_key(pos_id, pos_ot, benefit_keys, order_picture, delete_manual)

    def unset_applied_benefit(self, pos_id, order_state):
        # type: (int, List) -> None

        applied_benefits = self.order_service.get_applied_benefits_from_order_picture(pos_id)
        benefits = []
        tenders_remove = []
        discounts_remove = []
        item_amounts_remove = {}
        for applied_benefit in applied_benefits:
            benefit_applied = applied_benefit.get("benefit_applied")
            applied_order_state = applied_benefit.get("order_state")
            if benefit_applied and applied_order_state in order_state:
                item_amounts_remove.update(applied_benefit.get("discount_item_values", {}))
                tenders_remove.extend(applied_benefit.get("order_tenders_ids", "[]"))
                discounts_remove.extend(applied_benefit.get("used_discount_ids", "[]"))
                applied_benefit["benefit_applied"] = False
                applied_benefit["order_tenders_ids"] = []
                applied_benefit["used_discount_ids"] = []

            benefits.append(applied_benefit)

        model = get_model(pos_id)
        pos_ot = get_posot(model)
        self.order_service.clear_discounts(pos_ot, discounts_remove)
        self.order_service.clear_tenders(pos_id, pos_ot, tenders_remove)

        self._set_benefits_to_apply(
            pos_id,
            benefits,
            pos_ot,
            [],
            item_amounts_remove,
            tenders_remove,
            discounts_remove,
            delete_manual=False
        )

    def remove_invalid_benefits(self, pos_id, order_picture=None):
        # type: (int, Optional[eTree]) -> eTree

        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, benefits_remove, order_picture)

        return order_picture

    def _remove_benefit_by_key(self, pos_id, pos_ot, benefit_keys, order_picture=None, delete_manual=True):
        # type: (int, OrderTaker, List[str], Optional[eTree], Optional[bool]) -> None

        benefits_to_remove = self._get_applied_benefits_by_key(pos_id, benefit_keys, order_picture)

        self.check_if_benefit_is_removable(
            pos_id=pos_id,
            benefits_to_remove=benefits_to_remove,
            order_picture=order_picture
        )

        added_sale_lines = []
        used_discount_ids = []
        used_order_tender_ids = []

        for benefit_to_remove in benefits_to_remove:
            added_sale_lines.extend(benefit_to_remove.get("added_sale_lines", []))
            used_discount_ids.extend(benefit_to_remove.get("used_discount_ids", []))
            used_order_tender_ids.extend(benefit_to_remove.get("order_tenders_ids", []))

        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_order_tender_ids)

        self._delete_benefit(benefit_keys, pos_ot, pos_id, order_picture, delete_manual)

    def check_if_benefit_is_removable(
        self,
        pos_id,                 # type: str
        benefits_to_remove,     # type: List[Dict]
        order_picture=None      # type: Optional[eTree.XML]
    ):
        # 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, pos_ot, action_result, is_manual=False, remove_manual_key=''):
        # type: (AddBenefitToApplyUnitOfWork, OrderTaker, Optional[ActionResult], Optional[bool], Optional[str]) -> 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 = self._get_new_benefit(uow, action_result, old_benefits, benefit_key, order_state)
        else:
            benefit_key = uow.benefit_order_info.benefit_key
            str_data = self._update_benefit(uow, action_result)

        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, str_data, benefit_key, item_values, tenders_ids, discount_ids, is_manual, remove_manual_key
        )
        self.order_service.save_benefit_list_custom_property(pos_ot, benefit_list)

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

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

    @staticmethod
    def _get_new_benefit(uow, action_result, old_benefits, benefit_key, order_state):
        # type: (AddBenefitToApplyUnitOfWork, Optional[ActionResult], List, str, str) -> str

        benefit_total_value = BenefitService._sum_discounts(uow.promotion_tenders, action_result.discount_item_values)
        new_benefits = [dict(
            benefit=uow.raw_benefit,
            benefit_id=uow.raw_benefit.get("name"),
            benefit_total_value=benefit_total_value,
            burnt_items=action_result.items_to_burn,
            added_sale_lines=uow.added_sale_lines,
            promotion_tender=uow.promotion_tenders,
            benefit_applier=uow.benefit_applier,
            used_discount_ids=action_result.discounts_id,
            order_tenders_ids=action_result.order_tenders,
            discount_item_values=action_result.discount_item_values,
            benefit_applied=action_result.benefit_applied,
            tender_id=uow.tender_id,
            order_state=order_state,
            benefit_key=benefit_key,
            times_to_apply=uow.benefit.times_to_apply)
        ]

        return json.dumps(old_benefits + new_benefits)

    def _update_benefit(self, uow, action_result):
        # type: (AddBenefitToApplyUnitOfWork, Optional[ActionResult]) -> str

        old_benefits = self.order_service.get_benefit_list_from_order_picture(uow.pos_id)
        benefit_list = json.loads(old_benefits.get("benefitList", "[]"))
        list_benefit = []
        for benefit in benefit_list:
            if benefit["benefit_key"] == uow.benefit_order_info.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
                benefit["tender_id"] = uow.tender_id

            list_benefit.append(benefit)
        return json.dumps(list_benefit)

    def _delete_benefit(self, benefit_keys, pos_ot, pos_id, order_picture=None, delete_manual=True):
        # type: (List[str], OrderTaker, int, Optional[eTree], Optional[bool]) -> None

        applied_benefits = self.order_service.get_applied_benefits_from_order_picture(pos_id, order_picture)
        after_deletion = []
        keys_remove = []
        item_amounts_remove = {}
        tenders_remove = []
        discounts_remove = []

        for raw_benefit in applied_benefits:
            benefit_json_key = raw_benefit.get("benefit_key")
            if benefit_json_key not in benefit_keys:
                after_deletion.append(raw_benefit)
                continue

            keys_remove.append(raw_benefit.get("benefit_key"))
            item_amounts_remove.update(raw_benefit.get("discount_item_values"))
            tenders_remove.extend(raw_benefit.get("order_tenders_ids"))
            discounts_remove.extend(raw_benefit.get("used_discount_ids"))

        self._set_benefits_to_apply(
            pos_id,
            after_deletion,
            pos_ot,
            keys_remove,
            item_amounts_remove,
            tenders_remove,
            discounts_remove,
            order_picture,
            delete_manual
        )

    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]
            delete_manual=True      # type: Optional[bool]
    ):
        # type: (...) -> None

        if not benefits:
            pos_ot.setOrderCustomProperty("BENEFIT_LIST", "clear")
            return

        data = json.dumps(benefits)
        benefit_list = self._get_benefit_list_to_remove(
            pos_id,
            data,
            keys_remove,
            item_amounts_remove,
            tenders_remove,
            discounts_remove,
            order_picture,
            delete_manual
        )
        self.order_service.save_benefit_list_custom_property(pos_ot, benefit_list)

    def _get_applied_benefits_by_key(self, pos_id, benefit_keys, order_picture=None):
        # type: (int, List[str], Optional[eTree]) -> List

        matching_benefits = []
        applied_benefits = self.order_service.get_applied_benefits_from_order_picture(pos_id, order_picture)
        for applied_benefit in applied_benefits:
            applied_benefit_key = applied_benefit.get("benefit_key")
            if applied_benefit_key in benefit_keys:
                matching_benefits.append(applied_benefit)

        if not matching_benefits:
            raise DiscountNotFound()

        return matching_benefits

    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, benefit, order_picture=None, on_total=False, table_id=None):
        # type: (int, Dict, Element, bool, str) -> Optional[Element]

        order_id = None
        uow = None
        try:
            uow = self.get_unit_of_work(pos_id, benefit, order_picture, table_id=table_id)
            order_id = uow.order.id
            self.apply_benefit(uow, on_total=on_total)
            return uow.order.order_picture
        except OrderValidationException as _:
            pass
        except Exception as _:
            self.logger.exception(self._get_error_message(order_id, uow.benefit))

    def _apply_pending_benefit(self, pos_id, benefit_order_info):
        # type: (int, OrderBenefitInfo) -> None

        order_id = None
        benefit = None
        try:
            benefit = benefit_order_info.benefit
            uow = self.get_unit_of_work(pos_id, benefit.raw)
            uow.benefit_order_info = benefit_order_info
            order_id = uow.order.id
            self.apply_benefit(uow, validate_benefits=False)

        except OrderValidationException as _:
            pass
        except Exception as _:
            self.logger.exception(self._get_error_message(order_id, benefit))

    @staticmethod
    def _get_error_message(
            order_id,    # type: int
            benefit      # type: Benefit
    ):
        # type: (...) -> str

        return "Error applying benefit for order {} - Benefit name: {}".format(str(order_id), benefit.name)

    @staticmethod
    def _is_order_taker(model):
        # type: (eTree) -> bool

        working_mode = model.find(".//WorkingMode")
        pos_function = working_mode.get("posFunction")

        if pos_function is not None and pos_function == "OT":
            return True
        return False

    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
            is_manual,          # type: bool
            remove_manual_key   # type: str
    ):
        # type: (...) -> BenefitList

        order_picture = uow.order.order_picture
        benefits_pre_total = self._get_benefits_pre_total(uow.pos_id, benefit_key, order_picture)
        manual_benefits_on_total = self._get_manual_benefits_on_total(uow, order_picture, is_manual, remove_manual_key)
        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(
            benefits_pre_total, manual_benefits_on_total, discount_item_values, discount_excluded, tender_excluded, data
        )

    def _get_benefit_list_to_remove(
            self,
            pos_id,                 # type: int
            data,                   # type: str
            benefit_key,            # 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]
            delete_manual=True      # type: Optional[bool]
    ):
        # 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, benefit_key, order_picture)
        manual_benefits_on_total = self._get_removed_manual_benefits(pos_id, benefit_key, order_picture, delete_manual)
        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(
            benefits_pre_total, manual_benefits_on_total, discount_item_values, discount_excluded, tender_excluded, data
        )

    def _get_benefits_pre_total(self, pos_id, benefit_key, order_picture=None):
        # type: (int, [str, List[str]], Element) -> List

        benefits = self.order_service.get_benefit_list_from_order_picture(pos_id, order_picture)
        if isinstance(benefit_key, List) and len(benefit_key) > 0 and len(benefits) > 0:
            return self._remove_list_properties_benefits(benefits, benefit_key, "benefitPreTotalSequence")

        if len(benefits) == 0:
            return [benefit_key]

        if order_picture.get("state") == "TOTALED":
            return benefits.get("benefitPreTotalSequence")

        pre_total_sequence = benefits.get("benefitPreTotalSequence")
        if len(benefit_key) > 0:
            pre_total_sequence.append(benefit_key)
        return pre_total_sequence

    def _get_manual_benefits_on_total(
            self,
            uow,                    # type: AddBenefitToApplyUnitOfWork
            order_picture=None,     # type: Optional[eTree.XML]
            is_manual=False,        # type: Optional[bool]
            remove_manual_key=''    # type: Optional[str]
    ):
        # type: (...) -> List

        benefits = self.order_service.get_benefit_list_from_order_picture(uow.pos_id, order_picture)
        manual_sequence = benefits.get("manualBenefitOnTotalSequence", [])
        if order_picture.get("state") != "TOTALED":
            return manual_sequence

        new_manual_sequence = []
        for item in manual_sequence:
            key, benefit = item.items()[0]
            if key != remove_manual_key:
                new_manual_sequence.append({key: benefit})

        benefit_key = uow.benefit_key
        if len(benefit_key) > 0 and is_manual:
            raw_benefit = uow.benefit.raw
            new_manual_sequence.append({benefit_key: raw_benefit})

        return new_manual_sequence

    def _get_removed_manual_benefits(
            self,
            pos_id,                 # type: int
            benefit_keys,           # type: List[str]
            order_picture,          # type: eTree.XML
            delete_manual=True      # type: Optional[bool]
    ):
        # type: (...) -> List[Dict]

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

        if len(benefits) == 0:
            return []

        manual_benefits = benefits.get("manualBenefitOnTotalSequence", [])
        filtered_manual_benefits = []
        for benefit in manual_benefits:
            key, value = benefit.items()[0]
            if key not in benefit_keys or not delete_manual:
                filtered_manual_benefits.append({key: value})

        return filtered_manual_benefits

    def _get_discount_item_values(self, pos_id, item_discount_values, order_picture=None):
        # type: (int, Dict, Element) -> List

        benefits = self.order_service.get_benefit_list_from_order_picture(pos_id, order_picture)
        return self._get_dict_data_remove(benefits, item_discount_values, "discountItemValues")

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

        benefits = self.order_service.get_benefit_list_from_order_picture(pos_id, order_picture)
        return self._get_list_data_remove(benefits, discount_ids, "discountIdTipExcluded")

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

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

        return self._get_list_data_remove(benefits, order_tenders_ids, "tenderIdTipExcluded")

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

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

        if is_dict_data:
            return self._remove_dict_properties_benefits(benefits, data, property_name)
        return self._remove_list_properties_benefits(benefits, data, property_name)

    @staticmethod
    def _get_benefit_key(benefit_id):
        # type: (str) -> str

        return "{}_{}".format(benefit_id, int(round(time.time() * 1000)))

    @staticmethod
    def _get_dict_data_remove(benefits, dict_data, property_name):
        # type: (List, Dict, str) -> Dict

        ids = benefits.get(property_name, {})
        for data_id in dict_data.keys():
            if data_id not in ids.keys():
                ids[data_id] = dict_data[data_id]

        return ids

    @staticmethod
    def _get_list_data_remove(benefits, list_data, property_name):
        # type: (Dict, List, str) -> List

        if len(benefits) == 0:
            return []

        ids = benefits.get(property_name, "[]")
        for data_id in list_data:
            if data_id not in ids:
                ids.append(data_id)

        return ids

    @staticmethod
    def _remove_list_properties_benefits(benefits, list_remove, property_name):
        # type: (Dict, List, str) -> List

        result = []
        keys = benefits.get(property_name, "[]")
        for key in keys:
            if key not in list_remove:
                result.append(key)

        return result

    @staticmethod
    def _remove_dict_properties_benefits(benefits, dict_remove, property_name):
        # type: (List, Dict, str) -> List

        result = {}
        body = benefits.get(property_name, {})
        for key in body.keys():
            if key not in dict_remove.keys():
                result[key] = body[key]

        return result

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

        for benefit in self.automatic_benefits[discount_type]:
            self._apply_automatic_benefit(
                pos_id=pos_id,
                benefit=benefit,
                table_id=table_id
            )

        order = self.order_service.get_order(pos_id)
        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)

    @staticmethod
    def _sum_discount_items(discount_item_values):
        # type: (Dict) -> float

        total_discount = 0
        for key, value in discount_item_values.items():
            if isinstance(value, float):
                total_discount += value
                continue

            total_discount += BenefitService._sum_discount_items(value)

        return total_discount

    @staticmethod
    def _sum_discounts(promotion_tender, discount_item_values):
        # type: (Dict, Dict) -> float

        sum_promotion_tender = sum([float(x.get("amount")) for x in promotion_tender])
        sum_discount_item_values = BenefitService._sum_discount_items(discount_item_values)

        return sum_promotion_tender + sum_discount_item_values
