import json

from logging import Logger
from xml.etree import cElementTree as eTree

from application.model.exceptions import ProductNotFound
from typing import (
    Dict,
    List,
    Optional,
)

from sysactions import get_model

from application.model.benefit import (
    Action,
    ActionResult,
    Benefit,
)
from application.model.conditions import Restrictions
from application.model.order import Order
from application.model.transactions import StaticUnitOfWork
from application.repository import NDiscountRepository
from application.service import OrderService


class AddBenefitToApplyUnitOfWork(object):
    def __init__(
            self,
            logger,                 # type: Logger
            pos_id,                 # type: int
            benefit,                # type: Benefit
            benefit_applier,        # type: str
            tender_id,              # type: str
            order_picture,          # type: eTree.Element
            order_service,          # type: OrderService
            repository,             # type: NDiscountRepository
            product_groups,         # type: Dict
            static_uow,             # type: StaticUnitOfWork
            user_auth_level=False,  # type: Optional[bool]
    ):
        # type: (...) -> None

        self.logger = logger
        self.pos_id = pos_id
        self.benefit = benefit
        self.benefit_key = None
        self.benefit_id = benefit.id
        self.raw_benefit = benefit.raw
        self.benefit_applier = benefit_applier
        self.tender_id = tender_id
        self.order_service = order_service
        self.repository = repository
        self.product_groups = product_groups
        self.added_sale_lines = []
        self.promotion_tenders = []
        self.order_tenders_ids = []
        self.benefit_order_info = None
        self.user_auth_level = user_auth_level

        self.model = static_uow.model if static_uow is not None else get_model(pos_id)
        self.static_uow = static_uow

        self.order = self.order_service.get_order(
            pos_id=self.pos_id,
            create_order=any([a.name == "itemsToAdd" for a in self.benefit.actions]),
            order_picture=order_picture,
            model=self.model,
            static_uow=self.static_uow,
        )

        if self.static_uow is None:
            self.static_uow = StaticUnitOfWork(
                pos_id=pos_id,
                order_picture=self.order.order_picture,
                model=self.model,
            )

        self.pos_ot = self.static_uow.pos_ot

    def update_order(
        self,
        order_picture=None,     # type: Optional[eTree.Element]
    ):
        # type: (...) -> None

        self.order = self.order_service.get_order(
            pos_id=self.pos_id,
            create_order=any([a.name == "itemsToAdd" for a in self.benefit.actions]),
            order_picture=order_picture,
            model=self.model,
            static_uow=self.static_uow,
        )
        self.static_uow.set_order_picture(order_picture=self.order.order_picture)

    def add_sale_line(
        self,
        part_code,      # type: int
        is_free,        # type: bool
        price,          # type: float
        tender_id,      # type: int
        burnt_items,    # type: Dict
        restriction,    # type: Restrictions
        on_total,       # type: bool
        action,         # type: Action
    ):
        # type: (...) -> Dict

        if int(part_code) not in self.repository.get_all_products():
            raise ProductNotFound()

        items_to_burn = self.order.get_items_to_burn(
            order=self.order,
            benefit=self.benefit,
            burnt_items=burnt_items,
            product_groups=self.product_groups,
        )
        required_items = Order.get_required_items(
            order=self.order,
            benefit=self.benefit,
        )

        if on_total:
            properties = self._set_item_custom_properties(
                items_to_burn=items_to_burn,
                required_items=required_items,
                action=action,
                restriction=restriction,
            )
        else:
            properties = self._set_item_custom_properties(
                items_to_burn=items_to_burn,
                required_items=required_items,
                action=action,
            )

        added_line_number = self._sale_item(
            item_id=part_code,
            quantity=1,
            on_total=False,
            properties=properties,
        )

        self._overwrite_item_price(
            added_sale_line_number=added_line_number,
            price=price,
            is_free=is_free,
        )
        self.update_order()

        added_sale_lines = [x for x in self.order.sale_lines if int(x.line_number) == int(added_line_number)]
        tender_amount = self._get_tender_amount(added_sale_lines, is_free)

        if tender_amount > 0:
            self.promotion_tenders.append(
                {
                    "action_id": action.order,
                    "amount": tender_amount,
                    "tender_id": tender_id if tender_id is not None else self.tender_id
                }
            )

        self._get_item_to_burn_from_sale_lines(self.benefit, added_sale_lines, items_to_burn, part_code)
        return items_to_burn

    def update_promotion_tenders(
        self,
        action,         # type: Action
        action_result,  # type: ActionResult
    ):
        # type: (...) -> None

        order_tender_ids = action_result.order_tenders
        if not order_tender_ids:
            return

        self.update_order()

        for order_tender_id in order_tender_ids:
            self.promotion_tenders.append({
                "action_id": action.order,
                "amount": self.order.tenders[order_tender_id],
                "tender_id": order_tender_id or None,
            })

    @staticmethod
    def _get_item_to_burn_from_sale_lines(benefit, added_sale_lines, items_to_burn, part_code):
        if not benefit.conditions.burn_items:
            return

        for sale_line in added_sale_lines:
            if sale_line.item_id == str(part_code):
                key = "{}.{}.{}.{}".format(sale_line.line_number, sale_line.level, sale_line.context, sale_line.item_id)
                if key in items_to_burn:
                    items_to_burn[key] += sale_line.qty
                else:
                    items_to_burn[key] = sale_line.qty

    def _set_item_custom_properties(
        self,
        items_to_burn,      # type: Dict
        required_items,     # type: List
        action,             # type: Action
        restriction=None,   # type: Optional[Restrictions]
    ):
        # type: (...) -> Dict

        seat = action.seat or (action.value[0].get("seat", None) if type(action.value) == list else None)
        properties = {"benefitKey": self.benefit_key}
        if seat:
            properties.update({"seat": str(seat)})
        self._set_discount_parent_sale_line(items_to_burn, required_items, properties)
        self._set_item_seat_and_service_period(items_to_burn, required_items, properties)
        self._set_item_sale_type(
            properties=properties,
            restriction=restriction,
        )
        self._set_item_brand(
            properties=properties,
            restriction=restriction,
        )

        return properties

    def _set_discount_parent_sale_line(self, items_to_burn, required_items, properties):
        # type: (Dict, List, Dict) -> None

        items = None
        if items_to_burn:
            items = [x for x in self.order.sale_lines if x.key == next(iter(items_to_burn))]
        elif len(required_items) > 0:
            items = [x for x in self.order.sale_lines if x.key == next(iter(required_items))]

        if items:
            line_number = items[0].line_number
            properties.update({"relationLineNumber": line_number})

    def _set_item_seat_and_service_period(self, items_to_burn, required_items, properties):
        # type: (Dict, List, Dict) -> None

        items = None
        if items_to_burn:
            items = [x for x in self.order.sale_lines if x.key == next(iter(items_to_burn))]
        elif len(required_items) > 0:
            items = [x for x in self.order.sale_lines if x.key == next(iter(required_items))]

        if items:
            seat = items[0].seat
            if seat:
                properties.update({"seat": seat})

            service_period = items[0].service_period.str_json
            if service_period:
                properties.update({"servicePeriod": service_period})

    def _set_item_sale_type(self, properties, restriction=None):
        # type: (Dict, Restrictions) -> None

        if restriction and restriction.valid_sale_types and len(restriction.valid_sale_types) > 0:
            sale_type = restriction.valid_sale_types[0]
        else:
            sale_type = self.order.sale_type if self.order.sale_type != "EAT_IN" else None

        if sale_type and self.order.table_id:
            properties.update({"saleType": sale_type})

    def _set_item_brand(
        self,
        properties,         # type: Dict
        restriction=None,   # type: Optional[Restrictions]
    ):
        # type: (...) -> None

        brand = self._get_valid_order_brand(restriction=restriction)
        if brand:
            properties.update({"brand": brand})

    @staticmethod
    def _get_tender_amount(added_sale_lines, is_free):
        # type: (List, bool) -> float

        tender_amount = 0
        for sale_line in added_sale_lines:
            sale_line_price = sale_line.qty * sale_line.item_price
            if sale_line_price > 0 and is_free:
                tender_amount += sale_line_price

        return tender_amount

    def _overwrite_item_price(
            self,
            added_sale_line_number,   # type: int
            price,                    # type: Optional[float]
            is_free                   # type: bool
    ):
        # type (...) -> None

        if not is_free:
            self.pos_ot.priceOverwrite(
                posid=self.pos_id,
                linenumber=added_sale_line_number,
                unitprice=str(price if price is not None else ''),
            )
            self.logger.info("Price overwritten. New Price: {}".format(str(price)))

    def _sale_item(
        self,
        item_id,        # type: int
        quantity,       # type: int
        on_total,       # type: bool
        properties,     # type: Dict
    ):
        # type: (...) -> int

        after_total = "1" if on_total else "0"
        context_id = "1.%s" % item_id

        params = [self.pos_id, context_id, self.static_uow.price_list, quantity]
        sale_response = self.pos_ot.doSale(
            *params,
            aftertotal=after_total,
            applydefopts=True,
            itemproperties=json.dumps(properties)
        )
        added_sale_line = eTree.XML(sale_response)
        added_sale_line_number = int(added_sale_line.get("lineNumber"))

        self.added_sale_lines.append(added_sale_line_number)
        self.logger.info("New sale line added. LineNumber: {}; ItemId: {}".format(added_sale_line_number, context_id))

        return added_sale_line_number

    def set_benefit_key(self, benefit_key):
        self.benefit_key = benefit_key

    def _get_valid_order_brand(
        self,
        restriction=None,   # type: Optional[Restrictions]
    ):
        # type: (...) -> Optional[str]

        order = self.order

        if order.brand:
            return order.brand

        for sale_line in order.sale_lines:
            if sale_line.brand and sale_line.brand in restriction.valid_brands:
                return sale_line.brand

        return None
