# -*- coding: utf-8 -*-
import json
import logging
from xml.etree.ElementTree import Element

import persistence
import pyscripts
import re
from datetime import (
    datetime,
    timedelta,
)
from xml.etree import cElementTree as eTree

from bustoken import (
    TK_ELECTRONIC_PAYMENT_GET_DOCUMENT,
    TK_GET_USED_SERVICE,
)
from msgbus import (
    FM_PARAM,
    TK_SYS_ACK,
)
from posot import OrderTaker
from sysactions import (
    show_keyboard,
    show_messagebox,
    get_posot,
    get_model,
    get_current_order,
    translate_message,
    get_cfg,
    StopAction,
)

from typing import (
    Any,
    Dict,
    List,
    Optional,
    Tuple,
    Union,
)

from mw_helper import (
    format_currency,
    show_listbox_with_disabled_options,
    show_message_dialog,
)

from api.product import get_product_service
from application.domain import SaleLine
from model.customexception import ErrorGettingDevice

from pos_model import ProductType

mbcontext = pyscripts.mbcontext
logger = logging.getLogger("TableActions")

cached_tender_types = None
card_brands_cache = None
customer_info_config = {}

NOT_ELECTRONIC_TYPE_ID = (0, 9)


def update_custom_properties(
    customer_doc=None,             # type: Optional[str]
    customer_name=None,            # type: Optional[str]
):
    # type: (...) -> Dict[str, str]

    dict_sale = {}

    if customer_doc is not None:
        dict_sale.update({"CUSTOMER_DOC": customer_doc})
    if customer_name is not None:
        dict_sale.update({"CUSTOMER_NAME": customer_name})

    return dict_sale


def parse_loyalty_customer_info(
    loyalty_customer_info=None,     # type: Optional[str]
):
    # type: (...) -> Dict[str, str]

    customer_info = json.loads(loyalty_customer_info or "{}")
    customer_benefits = customer_info.pop("benefits", [])
    customer = customer_info.get("customerInfo", {}).get("customer")
    customer["isValidDocument"] = validate_document(customer_doc=customer.get("document", "")) is not False
    customer_info.get("customerInfo", {}).update(customer)

    result = dict(LOYALTY_CUSTOMER_INFO=customer_info)

    if customer_benefits:
        result["LOYALTY_CUSTOMER_BENEFITS"] = customer_benefits
        benefits_on_total = [
            benefit.get("id") for benefit in customer_benefits
            if benefit.get("automatic") == "ON_TOTAL"
        ]
        if benefits_on_total:
            result["LOYALTY_COUPONS_TO_APPLY_ON_TOTAL"] = benefits_on_total

    return result


def get_customer_doc_after_paid(
    pos_id,                     # type: str
    pos_ot,                     # type: OrderTaker
    ask_document_pinpad=False,  # type: Optional[bool]
):
    # type: (...) -> tuple

    document = get_custom_property_value(pos_id, "CUSTOMER_DOC")
    if document:
        return document, False

    valid = False
    customer_doc = None
    while not valid:
        if ask_document_pinpad:
            customer_doc = input_document_pinpad_dialog(pos_id)

        if customer_doc is None:
            customer_doc = show_keyboard(
                posid=pos_id,
                message="$ONLY_NUMBER",
                title="$ENTER_CPF_CNPJ",
                buttons="$OK|$NOT_INFORM",
                mask="CPF_CNPJ",
                numpad=True,
            )

        if customer_doc == "NOT_INFORM":
            customer_doc = ""

        if customer_doc == "":
            ret = show_messagebox(pos_id, "$CONFIRM_EMPTY_CPF_CNPJ", buttons="$YES|$NO")
            if ret == 1:
                raise StopAction()
            break
        customer_doc = str(customer_doc)
        valid = validate_document(customer_doc)
        if not valid:
            show_messagebox(pos_id, "$INVALID_CPF_CNPJ")
            customer_doc = None

    fill_additional_info(pos_ot, "CPF", customer_doc)
    return customer_doc, True


def electronic_payment_get_document(
    pos_id      # type: str
):
    # type: (...) -> Optional[str]

    component_name = get_device_service(
        pos_id=pos_id,
        device_type="eft",
    )
    if not component_name:
        return None

    try:
        result = mbcontext.MB_EasySendMessage(
            dest_name=component_name,
            token=TK_ELECTRONIC_PAYMENT_GET_DOCUMENT,
            data=pos_id
        )
        if result.token == TK_SYS_ACK:
            return result.data

        return None

    except (Exception,):
        logger.exception("Error ask documents from pinpad")
        return None


def input_document_pinpad_dialog(
    pos_id  # type: str
):
    # type: (...) -> Optional[str]

    option = show_messagebox(
        pos_id,
        message="$ASK_CPF_CNPJ",
        buttons="$CANCEL_WITHOUT_ICON|$NO_WITHOUT_ICON|$MANUAL|$PINPAD"
    )

    if option == 0:
        raise StopAction()
    elif option == 1:
        customer_doc = "NOT_INFORM"
    elif option == 2:
        customer_doc = None
    else:
        customer_doc = electronic_payment_get_document(pos_id)

    return customer_doc


def validate_document(customer_doc):
    valid = False
    if len(customer_doc) <= 11:
        valid = validar_cpf(customer_doc)
    elif len(customer_doc) > 11:
        valid = validar_cnpj(customer_doc)
    return valid


def fill_additional_info(pos_ot, properties, value):
    if pos_ot.additionalInfo:
        pos_ot.additionalInfo += "|"
    else:
        pos_ot.additionalInfo = ""
    pos_ot.additionalInfo += "{0}={1}".format(properties, value)


def validar_cpf(
    cpf,    # type: str
):
    # type: (...) -> bool

    cpf = "".join(re.findall(r"\d", str(cpf)))

    if (not cpf) or (len(cpf) < 11):
        return False

    inteiros = map(int, cpf)
    novo = inteiros[:9]

    if all(num == inteiros[0] for num in inteiros):
        return False

    while len(novo) < 11:
        r = sum([(len(novo) + 1 - i) * v for i, v in enumerate(novo)]) % 11

        if r > 1:
            f = 11 - r
        else:
            f = 0
        novo.append(f)

    if novo == inteiros:
        return cpf

    return False


def validar_cnpj(
    cnpj      # type: str
):
    # type: (...) -> bool

    cnpj = "".join(re.findall(r"\d", str(cnpj)))

    if (not cnpj) or (len(cnpj) < 14):
        return False

    inteiros = map(int, cnpj)
    novo = inteiros[:12]

    if all(num == inteiros[0] for num in inteiros):
        return False

    prod = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]
    while len(novo) < 14:
        r = sum([x * y for (x, y) in zip(novo, prod)]) % 11
        if r > 1:
            f = 11 - r
        else:
            f = 0
        novo.append(f)
        prod.insert(0, 6)

    if novo == inteiros:
        return cnpj

    return False


def json_serialize(dictionary):
    # type: (Dict) -> str
    try:
        return json.dumps(dictionary, default=lambda o: getattr(o, "__dict__", str(o)))
    except ValueError:
        return dictionary


def compare_objects(obj1, obj2):
    return object_to_json(obj1) == object_to_json(obj2)


def object_to_json(obj):
    return json.dumps(obj, sort_keys=True, default=lambda o: getattr(o, "__dict__", str(o)))


def get_seconds_to_renew_cache(seconds_to_renew_cache):
    seconds_to_renew = 60 * 5
    try:
        seconds_to_renew = int(seconds_to_renew_cache)
    except TypeError:
        pass
    return seconds_to_renew


def reached_seconds_to_renew_cache(last_cache_time, seconds_to_renew_cache):
    return not last_cache_time or datetime.now() > last_cache_time + timedelta(seconds=seconds_to_renew_cache)


def get_custom_property_value(
    pos_id,                 # type: str
    custom_property,        # type: str
    order_id=None,          # type: Optional[str]
    order_picture=None,     # type: Optional[eTree.XML]
    default_value=None,     # type: Optional[str]
):
    # type: (...) -> Optional[str]

    model = get_model(pos_id)
    pos_ot = get_posot(model)

    if not order_picture:
        if not order_id:
            order_picture_by_pos_id = pos_ot.orderPicture(pos_id)
            if order_picture_by_pos_id:
                order_picture = eTree.XML(order_picture_by_pos_id)
        else:
            order_picture = eTree.XML(pos_ot.orderPicture(pos_id, order_id))

    if not order_picture:
        return None
    current_property = order_picture.find(".//CustomOrderProperties/OrderProperty[@key='{}']".format(custom_property))
    return default_value if current_property is None else current_property.get("value")


def mask_phone(
        phone_number  # type: str
):
    # type: (...) -> str

    phone_number_size = len(phone_number)

    if phone_number_size in (8, 9):
        return "{}-{}".format(
            phone_number[:-4],
            phone_number[-4:]
        )
    if phone_number_size in (10, 11):
        return "({}) {}-{}".format(
            phone_number[0:2],
            phone_number[2:-4],
            phone_number[-4:]
        )

    return phone_number


def is_valid_string(
    string,             # type: str
    match_string=None   # type: Optional[str]
):
    # type: (...) -> bool

    if not match_string:
        match_string = "^[_a-zA-Z0-9 ,.-]+$"
    return bool(re.match(match_string, string))


def get_card_brands():
    # type: () -> Dict

    global card_brands_cache
    if card_brands_cache is not None:
        return card_brands_cache

    card_brands = {}
    conn = None
    try:
        conn = persistence.Driver().open(
            mbcontext,
            service_name="FiscalPersistence"
        )
        cursor = conn.pselect("fiscal_getBandeiras")
        for row in cursor:
            card_brands.update({int(row.get_entry("Bandeira")): row.get_entry("Descricao")})
    except (Exception,):
        logger.exception("Error getting cards brands")
    finally:
        if conn:
            conn.close()

    card_brands_cache = card_brands
    return card_brands_cache


def _count_option_listbox(
    option_text,    # type: str
    value,          # type: float
    options_dict    # type: List[Dict]
):
    # type: (...) -> int

    count = 0
    for option in options_dict:
        if option_text == option["text"] and value == option["value"]:
            count += 1

    return count


def options_dict_to_str_list(
    model,      # type: eTree.XML
    options     # type: List[Dict]
):
    # type: (...) -> List[str]

    listbox = []
    for option in options:
        formatted_value = format_currency(
            model=model,
            value=option["value"],
        )
        count = _count_option_listbox(
            option["text"],
            option["value"],
            options
        )
        if count > 1:
            option_text = "{0}: {1} ({2})".format(
                option["text"],
                formatted_value,
                option["count"]
            )
        else:
            option_text = "{0}: {1}".format(
                option["text"],
                formatted_value
            )
        listbox.append(option_text)

    return listbox


def check_has_tef(
    tenders,    # type: eTree.XML
):
    # type: (...) -> bool

    for tender in tenders:
        if is_electronic_payment(tender):
            return True

    return False


def get_tenders_options_dict(
    pos_id,                         # type: str
    model,                          # type: eTree.XML
    tenders,                        # type: eTree.XML
    card_brands,                    # type: Dict[int, str]
    group_digital_payments=False,   # type: bool
    total=0.0,                      # type: Optional[float]
):
    # type: (...) -> Tuple[List[Dict], List]

    if total:
        all_tenders_option = {
            "text": translate_message(
                model,
                "ALL_TENDERS"
            ),
            "value": total,
            "count": 1
        }
        options_dict = [all_tenders_option]
        initial_tef_index = 1
    else:
        options_dict = []
        initial_tef_index = 0

    disabled_items_index = []
    tef_not_available = get_device_service(
        pos_id=pos_id,
        device_type="eft",
    ) is None

    if group_digital_payments and check_has_tef(tenders):
        digital_payments_option = {
            "text": translate_message(
                model,
                "ELECTRONIC_PAYMENTS"
            ),
            "value": 0,
            "count": 1,
            "tenders": []
        }
        options_dict.append(digital_payments_option)

    option_text = ""
    for tender_index, tender in enumerate(tenders):
        value = float(tender.get("tenderAmount"))
        if value == 0:
            continue

        is_tef = is_electronic_payment(tender)
        if is_tef and tef_not_available:
            disabled_items_index.append(str(tender_index + 1))

        if is_tef:
            if not group_digital_payments:
                option_text = _get_option_text(
                    tender,
                    card_brands
                )
            else:
                options_dict[initial_tef_index]["value"] += float(value)
                options_dict[initial_tef_index]["tenders"] = options_dict[initial_tef_index].get("tenders") or []
                options_dict[initial_tef_index]["tenders"].append(tender.get("tenderId"))
        else:
            option_text = tender.get("tenderDescr").encode("utf-8")

        if not is_tef or (is_tef and not group_digital_payments):
            count = _count_option_listbox(
                option_text,
                value,
                options_dict
            )
            new_option = {
                "text": option_text,
                "tender_index": tender_index,
                "value": value,
                "count": count + 1
            }
            options_dict.append(new_option)

    if len(disabled_items_index) > 0:
        disabled_items_index.insert(0, "0")

    return options_dict, disabled_items_index


def _get_option_text(
    tender,         # type: eTree.Element
    card_brands,    # type: Dict[int, str]
):
    detail = json.loads(tender.get("tenderDetail"))
    tender_description = tender.get("tenderDescr").encode("utf-8")
    brand_code = int(detail["Bandeira"])
    brand = card_brands[int(detail["Bandeira"])] if brand_code in card_brands else ""
    card_number = str(detail["CardNumber"][:4] if "None" not in detail["CardNumber"][:4] else None)

    if card_number in ["", None]:
        return "{0} / {1}".format(
            tender_description,
            brand
        )

    return "{0} / {1} / {2}".format(tender_description, card_number, brand)


def get_order_tender(
    order_picture,  # type: eTree.XML
    tender_id       # type: str
):
    # type: (...) -> Optional[eTree.XML]

    tenders = order_picture.findall(".//TenderHistory/Tender")
    for tender in tenders:
        if tender.get("tenderId") == tender_id:
            return tender

    return None


def is_electronic_payment(
    tender   # type: Element
):
    # type: (...) -> bool

    return _get_electronic_type_id(tender) not in NOT_ELECTRONIC_TYPE_ID


def _get_electronic_type_id(
    tender   # type: Element
):
    # type: (...) -> int

    try:
        if type(tender) is dict:
            return int(tender.get("electronicTypeId"))
    except TypeError:
        pass

    if "electronicType" in tender:
        return int(tender["electronicType"])

    return int(tender.get("eletronicType"))


def get_customer_info_config(pos_id):
    global customer_info_config

    if int(pos_id) not in customer_info_config:
        document = get_cfg(pos_id).find_values("CustomerInfoRequest/Document") or []
        name = get_cfg(pos_id).find_values("CustomerInfoRequest/Name") or []

        customer_info_config[int(pos_id)] = {"document": document, "name": name}

    return customer_info_config[int(pos_id)]


def _translate_array(pos_id, messages):
    # type: (str, List[str]) -> List[str]

    model = get_model(pos_id)
    result = []
    for index, message in enumerate(messages, start=1):
        result.append("{} - {}".format(index, translate_message(model, message)))

    return result


def get_tender_info(
    tender_id=None,     # type: Optional[int]
):
    # type: (...) -> Optional[Any[Union[Dict, object]]]

    global cached_tender_types

    if cached_tender_types is None:
        product_service = get_product_service()
        cached_tender_types = product_service.get_tenders_dict()

    if tender_id is None:
        return cached_tender_types

    return cached_tender_types[tender_id] if tender_id in cached_tender_types else None


def _get_substring_occurrence(
    text,       # type: str
    character,  # type: str
):
    # type: (...) -> str

    first_occurrence = text.find(character)
    return character * (text.rfind(character) - first_occurrence + 1 if first_occurrence > -1 else 0)


def pad_barcode(
    barcode,   # type: str
):
    # type: (...) -> str

    barcode = barcode.replace(" ", "")
    barcode_2 = _get_substring_occurrence(
        text=barcode,
        character="2",
    )
    barcode_x = _get_substring_occurrence(
        text=barcode,
        character="X",
    )
    barcode_y = _get_substring_occurrence(
        text=barcode,
        character="Y",
    )
    barcode_w = _get_substring_occurrence(
        text=barcode,
        character="W",
    )

    return "{}{}{}{}{}".format(
        barcode_2,
        barcode_x,
        "0" * (13 - (len(barcode_2) + len(barcode_x) + len(barcode_y) + len(barcode_w))),
        barcode_y,
        barcode_w
    )


def get_sector_name(
    order,  # type: eTree
):
    # type: (...) -> Optional[str]

    sector = order.find(".//CustomOrderProperties/OrderProperty[@key='{}']".format("SECTOR"))
    if sector is not None:
        return sector.get("value", None)

    table_id = order.find(".//CustomOrderProperties/OrderProperty[@key='{}']".format("TABLE_ID"))
    if table_id is not None and "TAB-" in table_id.get("value", None):
        return "TAB"

    return None


def get_brand_name(
    order,  # type: eTree
):
    # type: (...) -> Optional[str]

    brand = order.find(".//CustomOrderProperties/OrderProperty[@key='BRAND']")
    if brand is not None:
        return brand.get("value", None)

    return None


def get_device_service(
    pos_id,         # type: str
    device_type,    # type: str
    sector=None,    # type: Optional[str]
    brand=None,     # type: Optional[str]
):
    # type: (...) -> str

    request = json.dumps(
        {
            "posId": pos_id,
            "deviceType": device_type,
            "sector": sector,
            "brand": brand,
        }
    )

    msg = mbcontext.MB_EasySendMessage(
        dest_name="DeviceManager",
        token=TK_GET_USED_SERVICE,
        format=FM_PARAM,
        data=request,
    )

    if msg.token == TK_SYS_ACK:
        return msg.data

    exception = ErrorGettingDevice(
        pos_id=pos_id,
        device_type=device_type,
    )
    raise exception


def get_device_service_error_message(
    model,          # type: str
    exception,      # type: ErrorGettingDevice
):
    message_device_text = translate_message(model, "DEVICE_NOT_FOUND")
    message_device_type = translate_message(model, exception.device_type.upper())
    return "{}: {}".format(message_device_text, message_device_type)


def send_order_canceled_event(
    pos_id,       # type: str
    order_id="",  # type: str
):
    # type: (...) -> None

    try:
        if not order_id and pos_id:
            model = get_model(posid=pos_id)
            order_id = get_current_order(model=model).get("orderId")

        event_data = {
            "posId": pos_id,
            "orderId": order_id,
        }
        mbcontext.MB_EasyEvtSend(
            subject="ORDER_CANCELED",
            type="",
            xml=json.dumps(event_data),
        )
        logger.info("ORDER_CANCELED event sent successfully! order_id={0}".format(order_id))
    except (Exception,):
        logger.exception("Error sending ORDER_CANCELED event")


def parse_qr_tag(text):
    try:
        root = eTree.fromstring("<p>" + text + "</p>")
        message = "" if root.text is None else root.text.strip()
        qr_data = None
        tag = root.find("qr")
        if tag is not None:
            qr_data = "" if tag.text is None else tag.text.strip()
    except Exception:  # noqa
        logger.exception("error parsing qr tag for customer final message: %r", text)
        message = text
        qr_data = None
    return message, qr_data


def is_table_service(
    model,  # type: eTree.Element
):
    # type: (...) -> bool

    order_properties = model.findall(".//CustomOrderProperties/OrderProperty")
    for order_property in order_properties:
        if order_property.get("key") == "TABLE_ID":
            return True

    return False


def ask_pager_number(
    pos_id,  # type: str
):
    # type: (...) -> str

    valid = False
    pager_number = None
    while not valid:
        message = "$ONLY_NUMBERS"
        title = "$ENTER_PAGER_NUMBER"
        pager_number = show_keyboard(
            posid=pos_id,
            message=message,
            title=title,
            buttons="$OK|$NOT_INFORM",
            numpad=True,
        )
        if pager_number == "NOT_INFORM":
            pager_number = None

        valid = validate_pager_number(
            pager_number=pager_number,
        )
        if not valid:
            pager_number = None
            show_messagebox(
                posid=pos_id,
                message="$INVALID_PAGER_NUMBER",
            )

    return pager_number


def validate_pager_number(
    pager_number,  # type: str
):
    # type: (...) -> bool

    if pager_number == "":
        return False

    return True


def show_order_tenders_options(
    pos_id,                    # type: str
    listbox_options,           # type: List[str]
    is_disabled=False,         # type: Optional[bool]
    has_receipt_card=True,     # type: Optional[bool]
):
    # type: (...) -> Dict

    if is_disabled:
        buttons = "$CLEAR_TENDERS|$PREVIOUS"
    else:
        disable_reprint = "" if has_receipt_card else "#DISABLED"
        buttons = "$REPRINT_RECEIPT{}|$CLEAR_TENDERS|$PREVIOUS".format(disable_reprint)

    selected_options = show_listbox_with_disabled_options(
        pos_id,
        listbox_options,
        buttons=buttons,
        message="$SELECT_TENDER",
    )

    return selected_options


def is_stored_order(
    model,          # type: eTree.Element
    pos_id=None,    # type: int
    order_id=None,  # type: int
):
    # type (...) -> bool

    order_picture = model.find("CurrentOrder/Order")
    if order_picture is None and order_id:
        pos_ot = get_posot(model)
        order_picture = eTree.XML(pos_ot.orderPicture(
            posid=pos_id,
            orderid=order_id,
        ))

    order_states = (order_picture.findall(".//StateHistory/State") or []) if order_picture is not None else []
    return any([True for state in order_states if state.get("state") == "STORED"])


def is_order_in_progress(
    model,  # type:  eTree.Element
):
    # type (...) -> bool

    current_order = model.find("CurrentOrder/Order")
    if current_order is None:
        return False

    return current_order.get("state") == "IN_PROGRESS"


def tender_has_receipt_card(
    tenders,    # type:  eTree
):
    # type: (...) -> bool

    for tender in tenders:
        tender_detail = _get_receipt_to_print(
            tender=tender,
        )
        if tender_detail:
            return True

    return False


def _get_receipt_to_print(
    tender,     # type: eTree
):
    # type: (...) -> Optional[Dict]

    if int(tender.get("eletronicType")) in (0, 9):
        return None

    tender_detail = json.loads(tender.get("tenderDetail"))
    receipt_customer = tender_detail.get("ReceiptCustomer", None)
    amount = tender_detail.get("tenderAmount", 0)

    if receipt_customer not in ["", None] and amount > 0:
        return tender_detail


def build_dict(
    key,               # type: str
    value,             # type: str
    dict_data=None,    # type: Optional[Dict]
):
    # type: (...) -> Dict

    if dict_data is None:
        dict_data = {}

    if value is not None:
        dict_data[key] = value
    return dict_data


def get_translated_exception_message(
    model,              # type: eTree.Element
    exception_message,  # type: str
):
    # type: (...) -> str

    message_to_translate = exception_message.split("$")
    if len(message_to_translate) > 1:
        message_to_translate = message_to_translate[-1]
    message = translate_message(model, message_to_translate)
    return message


def build_clean_customer_props_dict():
    # type: (...) -> dict

    return {
        "CUSTOMER_DOC": "clear",
        "CUSTOMER_NAME": "clear",
        "CUSTOMER_PHONE": "clear",
        "ADDRESS_REFERENCE": "clear",
        "CITY": "clear",
        "POSTAL_CODE": "clear",
        "COMPLEMENT": "clear",
        "NEIGHBORHOOD": "clear",
        "STATE": "clear",
        "STREET_NAME": "clear",
        "STREET_NUMBER": "clear",
    }


def get_pos_id_from_session(
    session_id,     # type: str
):
    # type: (...) -> str

    for data in session_id.split(","):
        if data.startswith("pos="):
            return data.split("=")[1]

    return ""


def get_user_id_from_session(
    session_id,     # type: str
):
    # type: (...) -> str

    for data in session_id.split(","):
        if data.startswith("user="):
            return data.split("=")[1]

    return ""


def get_session_id_info(
    session_id,         # type: str
):
    # type: (...) -> Dict[str, str]

    session_id_info = {}
    for session_data in session_id.split(","):
        key, value = session_data.split("=")
        session_id_info[key] = value

    return session_id_info


def get_sale_line_to_tag(
    model,      # type: eTree.Element
    sale_line,  # type: Dict[str, Any]
):
    # type: (...) -> Dict[str, Any]

    if sale_line["itemType"] == ProductType.PRODUCT:
        return _get_sale_line_to_tag_by_product(
            model=model,
            sale_line=sale_line,
        )

    if sale_line["itemType"] == ProductType.OPTION:
        return _get_sale_line_to_tag_by_option(
            model=model,
            sale_line=sale_line,
        )

    if sale_line["itemType"] == ProductType.CANADD:
        return _get_sale_line_to_tag_by_can_add(
            model=model,
            sale_line=sale_line,
        )

    return sale_line


def get_benefit_total_value(
    benefit_id,         # type: str
    order_picture,      # type: eTree.XML
):
    # type: (...) -> float

    benefit_list_value = order_picture.find(".//CustomOrderProperties/OrderProperty[@key='BENEFIT_LIST']").get("value")
    benefit_list_value_json = json.loads(benefit_list_value)
    benefit_list = json.loads(benefit_list_value_json.get("benefitList"))
    total_value = 0
    for benefit in benefit_list:
        if benefit_id == benefit.get("benefit").get("id"):
            total_value = benefit.get("benefit_total_value")

    return total_value


def value_is_close_to(
    value,              # type: float
    close_to,           # type: float
    precision=1e-3,     # type: float
):
    # type: (...) -> bool

    return abs(value - close_to) < precision


def _get_parent(
    sale_line,  # type: Dict[str, Any]
):
    # type: (...) -> Optional[SaleLine]

    return _get_ancestor(
        sale_line=sale_line,
        index=-1,
    )


def _get_grand_parent(
    sale_line,  # type: Dict[str, Any]
):
    # type: (...) -> Optional[SaleLine]

    return _get_ancestor(
        sale_line=sale_line,
        index=-2,
    )


def _get_ancestor(
    sale_line,  # type: Dict[str, Any]
    index,      # type: int
):
    # type: (...) -> Optional[SaleLine]

    min_size = (-index) + 1
    divided_context = sale_line["itemId"].split(".")
    if len(divided_context) < min_size:
        return None

    ancestor_part_code = divided_context[index]
    ancestor_context = ".".join([s for s in divided_context[:index]])

    return SaleLine(
        seat_number=None,
        order_id=None,
        line_number=sale_line["lineNumber"],
        item_id=ancestor_context,
        level=None,
        part_code=ancestor_part_code,
    )


def _is_same_sale_line(
    sale_line,      # type: SaleLine
    sale_line_xml,  # type: eTree.Element
):
    # type: (...) -> bool

    line_number = sale_line_xml.get("lineNumber")
    context = sale_line_xml.get("itemId")
    part_code = sale_line_xml.get("partCode")

    base_line_number = sale_line.line_number
    base_context = sale_line.item_id
    base_part_code = sale_line.part_code

    return line_number == base_line_number and context == base_context and part_code == base_part_code


def _get_sale_line_to_tag_by_product(
    model,      # type: eTree.Element
    sale_line,  # type: Dict[str, Any]
):
    # type: (...) -> Dict[str, Any]

    parent_sale_line = _get_parent(sale_line=sale_line)
    if not parent_sale_line:
        return sale_line

    order = model.find("CurrentOrder/Order")
    sale_lines = order.findall("SaleLine")

    for current_sale_line in sale_lines:
        is_same_line = _is_same_sale_line(
            sale_line=parent_sale_line,
            sale_line_xml=current_sale_line,
        )
        if not is_same_line:
            continue

        item_type = current_sale_line.get("itemType")
        if item_type == ProductType.PRODUCT:
            return dict(current_sale_line.items())

    return sale_line


def _get_sale_line_to_tag_by_can_add(
    model,      # type: eTree.Element
    sale_line,  # type: Dict[str, Any]
):
    # type: (...) -> Dict[str, Any]

    grand_parent_sale_line = _get_grand_parent(sale_line=sale_line)
    if not grand_parent_sale_line:
        return sale_line

    order = model.find("CurrentOrder/Order")
    sale_lines = order.findall("SaleLine")

    for current_sale_line in sale_lines:
        is_same_line = _is_same_sale_line(
            sale_line=grand_parent_sale_line,
            sale_line_xml=current_sale_line,
        )
        if not is_same_line:
            continue

        item_type = current_sale_line.get("itemType")

        if item_type == ProductType.CANADD:
            return _get_sale_line_to_tag_by_can_add(
                model=model,
                sale_line=dict(current_sale_line.items()),
            )

        if item_type == ProductType.COMBO:
            return sale_line

        if item_type == ProductType.PRODUCT:
            return dict(current_sale_line.items())

    return sale_line


def _get_sale_line_to_tag_by_option(
    model,      # type: eTree.Element
    sale_line,  # type: Dict[str, Any]
):
    # type: (...) -> Dict[str, Any]

    parent_sale_line = _get_parent(sale_line=sale_line)
    if not parent_sale_line:
        return sale_line

    order = model.find("CurrentOrder/Order")
    sale_lines = order.findall("SaleLine")
    for current_sale_line in sale_lines:
        is_same_line = _is_same_sale_line(
            sale_line=parent_sale_line,
            sale_line_xml=current_sale_line,
        )
        if not is_same_line:
            continue

        item_type = current_sale_line.get("itemType")
        if item_type == ProductType.CANADD:
            return _get_sale_line_to_tag_by_can_add(
                model=model,
                sale_line=dict(current_sale_line.items()),
            )

        if item_type in [ProductType.PRODUCT, ProductType.COMBO]:
            return dict(current_sale_line.items())

    return sale_line


def show_applied_discount_and_pending_value_to_pay(
    pos_id,                 # type: str
    model,                  # type: eTree.Element
    discount_amount,        # type: float
    pending_value_to_pay,   # type: Union[str, float]
    tender_type,            # type: str
):
    # type: (...) -> None

    discount_amount_formatted = format_currency(
        model=model,
        value=discount_amount,
    )
    new_amount_formatted = format_currency(
        model=model,
        value=float(pending_value_to_pay),
    )

    message = "$DISCOUNT_APPLIED|{}|{}|{}".format(discount_amount_formatted, tender_type, new_amount_formatted)
    show_message_dialog(
        pos_id=pos_id,
        message=message,
    )
