# -*- 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_SLCTRL_OMGR_UPDATEORDERTENDERDETAIL,
    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_any_dialog,
    show_keyboard,
    show_messagebox,
    get_posot,
    get_model,
    get_current_order,
    translate_message,
    get_cfg,
    get_tender_types,
    StopAction,
)

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

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

cached_tender_types = None
card_brands_cache = None

NOT_ELECTRONIC_TYPE_ID = (0, 9)


def get_void_reason(pos_id, selected_index=None):
    void_reason_ids = range(1, 13)

    void_reason_options = get_void_reason_options(pos_id)
    void_reason_to_show = void_reason_options[0:4]
    if not selected_index:
        selected_index = show_any_dialog(pos_id, "filteredListBox", "", "$SELECT_REASON", "|".join(void_reason_to_show),
                                         "$OK|$CANCEL", "", 600000, "NOFILTER", "", "", "", None, False)

    if selected_index is None:
        return None

    selected_index = int(selected_index)
    void_reason_id = void_reason_ids[int(selected_index)]
    void_reason_descr = void_reason_options[int(selected_index)]
    void_reason_dict = {'VOID_REASON_ID': void_reason_id, 'VOID_REASON_DESCR': void_reason_descr}
    return void_reason_dict


def update_custom_properties(customer_doc=None, customer_name=None, pre_sale=None):
    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})
    if pre_sale not in (None, ""):
        dict_sale.update({"PRE_VENDA": pre_sale})

    return dict_sale


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

    customer_info_config = get_customer_info_config(pos_id)

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

    if "afterPaid" not in customer_info_config['document']:
        return "", True

    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(pos_id, "$ONLY_NUMBER", "$ENTER_CPF_CNPJ", mask="CPF_CNPJ", numpad=True)

        if customer_doc is None:
            return None, False

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

    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 as _:
        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 = ""
    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):
    cpf = ''.join(re.findall('\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):
    cnpj = ''.join(re.findall('\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 as _:
        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 as _:
        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: eTree.XML
):
    # type: (...) -> Optional[str]

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

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

    current_property = order_picture.find(".//CustomOrderProperties/OrderProperty[@key='{}']".format(custom_property))
    return current_property 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 as _:
        logger.exception("Error getting cards brands")
    finally:
        if conn:
            conn.close()

    card_brands_cache = card_brands
    return card_brands_cache


def format_currency(
    model,      # type: eTree.XML
    value       # type: float
):
    # type: (...) -> str

    currency_symbol = translate_message(
        model,
        "L10N_CURRENCY_SYMBOL"
    )

    value_str = "{0} {1:.2f}".format(
        currency_symbol,
        value
    )
    return value_str


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,
            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
    total,                          # type: float
    card_brands,                    # type: Dict[int, str]
    group_digital_payments=False    # type: bool
):
    # type: (...) -> Tuple[List[Dict], List]

    all_tenders_option = {
        "text": translate_message(
            model,
            "ALL_TENDERS"
        ),
        "value": total,
        "count": 1
    }

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

    options_dict = [all_tenders_option]
    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[1]["value"] += float(value)
                options_dict[1]["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 save_order_cancel_receipt(
    pos_id,         # type: str
    tender_id       # type: str
):
    # type: (...) -> bool

    model = get_model(pos_id)
    pos_ot = get_posot(model)
    order_picture = get_current_order(model)
    tender = get_order_tender(
        order_picture,
        tender_id
    )
    tender_details = json.loads(tender.get("tenderDetail"))
    receipt_cancel = {
        "posId": pos_id,
        "userId": model.find("Operator").get("id"),
        "canceledAt": str(datetime.now())
    }

    if "ReceiptCancel" in tender_details:
        tender_details["ReceiptCancel"].update(receipt_cancel)
    else:
        tender_details["ReceiptCancel"] = receipt_cancel

    try:
        params = (
            '1' if pos_ot.blkopnotify else '0',
            pos_id,
            order_picture.get("orderId"),
            json.dumps([{
                "tenderid": int(tender.get("tenderId")),
                "tenderdetail": json.dumps(tender_details)
            }])
        )
        req_buf = "\x00".join((str(p) for p in params))
        result = mbcontext.MB_EasySendMessage(
            "ORDERMGR%s" % pos_id,
            TK_SLCTRL_OMGR_UPDATEORDERTENDERDETAIL,
            FM_PARAM,
            req_buf,
            600000000
        )
        if result.token == TK_SYS_ACK:
            return True

    except Exception as _:
        logger.exception("Fail salve receipt cancel: Tender: {}".format(tender.get("tenderId")))

    return False


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

    if type(tender) is dict:
        return int(tender.get("electronicTypeId"))

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


def get_customer_info_config(pos_id):
    document = get_cfg(pos_id).find_values("CustomerInfoRequest/Document") or []
    name = get_cfg(pos_id).find_values("CustomerInfoRequest/Name") or []

    return {'document': document, 'name': name}


def get_void_reason_options(
    pos_id  # type: str
):
    # type: (...) -> List[str]

    void_reason_options = ["CHANGED_YOUR_MIND",
                           "DUPLICATE",
                           "WRONG_SALE",
                           "CANCELLATION",
                           "COUPON_CANCELED",
                           "PAYMENT_CANCELED",
                           "SAVED_ORDER_CANCELED",
                           "ERROR_CREATING_SALE",
                           "ERROR_PROCESSING_PAYMENT_SELECTION",
                           "CANCELLATION_VIA_SYSTEM",
                           "SYNC_ERROR",
                           "MANAGER_AUTHORIZATION_CANCELLATION"]

    return _translate_array(pos_id, void_reason_options)


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:
        cached_tender_types = get_tender_types()

    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_device_service(
    pos_id,         # type: str
    device_type,    # type: str
    sector=None,    # type: str
):
    # type: (...) -> str

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

    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

    raise Exception(msg.data)
