import json
import sysactions
import time

from abc import ABCMeta
from xml.etree import cElementTree as eTree

from dateutil import tz
from msgbus import (
    TK_SYS_ACK,
    FM_PARAM,
    TK_HV_COMPLIST,
    MBEasyContext,
    TK_HV_GLOBALRESTART,
)
from persistence import Connection, Driver
from sysactions import (
    show_any_dialog,
    show_keyboard,
    get_model,
    get_pricelist,
    get_podtype,
    get_custom,
    get_current_order,
    show_messagebox,
    translate_message,
)
from systools import sys_log_error
from typing import (
    Any,
    Callable,
    Dict,
    List,
    Optional,
    Union,
)

MODIFIER_PREFIXES = {}

class BaseRepository(object):
    __metaclass__ = ABCMeta

    def __init__(self, mbcontext):
        # type: (MBEasyContext) -> None
        self.mbcontext = mbcontext

    def execute_with_connection(self, method, db_name=None, service_name=None):
        # type: (Callable[[Connection], Any], int, str) -> Any
        conn = None
        try:
            conn = Driver().open(self.mbcontext,
                                 dbname=str(db_name) if db_name is not None else None,
                                 service_name=service_name)
            return method(conn)
        finally:
            if conn is not None:
                conn.close()

    def execute_with_transaction(self, method, db_name=None, service_name=None):
        # type: (Callable[[Connection], Any], int, str) -> Any
        def transaction_method(conn):
            # type: (Connection) -> Any

            conn.transaction_start()
            try:
                conn.query("BEGIN TRANSACTION")
                ret = method(conn)
                conn.query("COMMIT")
                return ret
            except:
                conn.query("ROLLBACK")
                raise
            finally:
                conn.transaction_end()

        return self.execute_with_connection(transaction_method, db_name=db_name, service_name=service_name)

    def execute_in_all_databases(self, method, pos_list):
        # type: (Callable[[Connection], Any], List[int]) -> Any
        ret = []
        for pos in pos_list:
            conn = None
            try:
                conn = Driver().open(self.mbcontext, dbname=str(pos))
                ret.append(method(conn))
            finally:
                if conn is not None:
                    conn.close()

        return ret

    def execute_in_all_databases_returning_flat_list(self, method, pos_list):
        # type: (Callable[[Connection], Any], List[int]) -> Any
        inner_ret = self.execute_in_all_databases(method, pos_list)

        ret = []
        for inner_list in inner_ret:
            ret.extend(inner_list)

        return ret


def show_message_dialog(pos_id, title="$INFORMATION", message="", mask="", assync=False):
    return show_any_dialog(pos_id, "messageDialog", message, title, "", "", "", 600000, mask, "", "", "", None, assync)


def show_message_options_dialog(pos_id, buttons, title="$INFORMATION", message="", timeout=600000):
    resp = show_any_dialog(
        pos_id, "messageOptionsDialog", message, title, "", buttons, "", timeout, "", "", "", "", None, False)

    if resp in (None, ''):
        return resp

    return int(resp)


def show_numpad_dialog(pos_id, title="", message="", mask="INTEGER", default_value=""):
    resp = show_keyboard(pos_id, message, title=title, mask=mask, numpad=True, defvalue=default_value)

    if resp in (None, '', 'NaN'):
        return resp

    mask = mask.lower()
    if mask == 'password' or mask == 'login':
        return str(resp)

    if mask != "currency" and mask != "number":
        return int(resp)

    return float(resp)


def show_filtered_list_box_dialog(pos_id, options, title="", message="", mask="", buttons="$OK|$CANCEL"):
    if type(options) == list:
        options = "|".join(options)
    resp = show_any_dialog(
        pos_id, "filteredListBox", message, title, options, buttons, "",  600000, mask, "", "", "", None, False)

    if resp in (None, ''):
        return resp

    return int(resp)


def show_check_list_box_dialog(pos_id, options, title="", message="", mask="", buttons="$OK|$CANCEL"):
    if type(options) == list:
        options = "||".join(options)
    resp = show_any_dialog(
        pos_id, "checkListBox", message, title, options, buttons, "",  600000, mask, "", "", "", None, False)

    if resp in (None, ''):
        return []

    resp = resp.split(';')
    return resp


def show_editable_list_box_dialog(pos_id, options="", title="", buttons="$OK|$CANCEL"):
    resp = show_any_dialog(
        pos_id, "multipleColumnListBox", "", title, options, buttons, "",  600000, "", "", "", "", None, False)

    return resp


def show_bordereau_dialog(pos_id):
    return show_any_dialog(
        pos_id, "bordereauDialog", '', "$BORDEREAU", "", "", "", 6000000, "mask", "", "", "", None, False)


def show_tab_customer_dialog(pos_id):
    return show_any_dialog(
        pos_id, "tabCustomerInfoDialog", '', "$CUSTOMER_INFO", "", "", "", 600000, "mask", "", "", "", None, False)


def show_confirm_dialog(pos_id, title=''):
    if not title:
        title = "$BORDEREAU"
    return show_any_dialog(
        pos_id, "bordereauDialog", 'confirmDialog', title, "", "", "", 3600000, "mask", "", "", "", None, False)


def show_qr_code(
    pos_id,             # type: str
    code_data,          # type: str
    buttons="$CANCEL",  # type: str
    title="",           # type: Optional[str]
    timeout=600000,     # type: Optional[int]
    info_text="",       # type: Optional[str]
):
    # type: (...) -> Optional[int]

    resp = show_any_dialog(
        pos_id,
        "qrcode",
        code_data,
        title,
        info_text,
        buttons,
        "",
        timeout,
        "",
        "",
        "",
        "",
        "",
        True
    )
    if resp in (None, ''):
        return 0

    return int(resp)


def show_listbox_with_disabled_options(
    pos_id,                         # type: str
    options,                        # type: List
    message='$SELECT_AN_OPTION',    # type: Optional[str]
    title='',                       # type: Optional[str]
    defvalue='',                    # type: Optional[str]
    buttons='$OK|$CANCEL',          # type: Optional[str]
    icon='',                        # type: Optional[str]
    timeout=60000,                  # type: Optional[int]
    mask=''                         # type: Optional[List]
):
    # type: (...) -> Optional[Dict]

    minvalue, maxvalue, contents, asynch = ('', '', None, False)
    info_text = '|'.join(options) if hasattr(options, '__iter__') else str(options)
    mask_text = '|'.join(mask) if hasattr(mask, '__iter__') else str(mask)
    data = show_any_dialog(
        posid=pos_id,
        dlgtype='listbox',
        message=message,
        title=title,
        infotext=info_text,
        buttons=buttons,
        icon=icon,
        timeout=timeout,
        mask=mask_text,
        defvalue=defvalue,
        minvalue=minvalue,
        maxvalue=maxvalue,
        contents=contents,
        asynch=asynch
    )
    if asynch:
        return listbox_response(
            selected_list_index=int(data),
        )
    elif data is None:
        return None
    split = data.split(',')
    buttons_quantity = len(buttons.split("|"))
    if int(split[0]) != buttons_quantity - 1 and split[1] not in mask:
        return listbox_response(
            selected_list_index=int(split[1]),
            selected_button_index=int(split[0]),
        )
    else:
        return None


def listbox_response(
    selected_list_index=None,       # type: Optional[int]
    selected_button_index=None,     # type: Optional[int]
):
    # type: (...) -> Dict

    listbox_response_dict = {
        "selected_list_index": selected_list_index,
        "selected_button_index": selected_button_index,
    }

    return listbox_response_dict


def show_device_manager_dialog(
    pos_id,                         # type: str
    info_text,                      # type: json
    message='',                     # type: Optional[str]
    title='$DEVICE_MANAGER',        # type: Optional[str]
    defvalue='',                    # type: Optional[str]
    buttons='$CANCEL|$UPDATE',      # type: Optional[str]
    icon='',                        # type: Optional[str]
    timeout=60000,                  # type: Optional[int]
):
    # type: (...) -> Optional[str]

    mask_text, minvalue, maxvalue, contents, asynch = ('', '', '', None, False)
    info_text = info_text.encode('UTF-8')
    data = show_any_dialog(
        posid=pos_id,
        dlgtype='deviceManager',
        message=message,
        title=title,
        infotext=info_text,
        buttons=buttons,
        icon=icon,
        timeout=timeout,
        mask=mask_text,
        defvalue=defvalue,
        minvalue=minvalue,
        maxvalue=maxvalue,
        contents=contents,
        asynch=asynch,
    )
    if data in (None, ''):
        return None

    return data


def show_loyalty_dialog(
    pos_id,        # type: str
    buttons,       # type: str
    message="",    # type: str
):
    # type: (...) -> Optional[Any[Union[int, str]]]

    resp = show_any_dialog(
        posid=pos_id,
        dlgtype='loyaltyDialog',
        message=message,
        title='$LOYALTY',
        infotext="",
        buttons=buttons,
        icon="",
        timeout=600000,
        mask="",
        defvalue="",
        minvalue="",
        maxvalue="",
        contents=None,
        asynch=False,
    )

    if resp in (None, ''):
        return resp

    return int(resp)


def components_is_running(mb_context, *params):
    msg_resp = mb_context.MB_EasySendMessage("HyperVisor", TK_HV_COMPLIST, FM_PARAM)
    if msg_resp.token != TK_SYS_ACK:
        sys_log_error("Unable to obtain the TK_HV_COMPLIST")
        return False

    component_list = [_.get('name').lower() for _ in eTree.XML(msg_resp.data).findall(".//component[@status='1']")]
    for component in params:
        if component.lower() not in component_list:
            return False

    return True


def ensure_iterable(value):
    if isinstance(value, basestring):
        return filter(None, [value])

    try:
        _ = (e for e in value)
        return filter(None, value)
    except TypeError:
        return filter(None, [value])


def ensure_list(value):
    if isinstance(value, basestring):
        return [value]

    if isinstance(value, list):
        return value

    try:
        _ = (e for e in value)
        return list(value)
    except TypeError:
        return [value]


def convert_to_dict(iterable):
    ret = {}
    for item in iterable:
        ret[item] = item
    return ret


def convert_from_localtime_to_utc(localtime):
    # type: (datetime) -> datetime
    from_zone = tz.tzlocal()
    to_zone = tz.tzutc()
    localtime = localtime.replace(tzinfo=from_zone)
    utc = localtime.astimezone(to_zone)

    return utc


def restart_system(
        mb_context,             # type: MBEasyContext
        event_type,             # type: str
        pos_id="",              # type: str
        menu_manager_user="",   # type: str
):
    # type: (...) -> MBMessage

    _notify_global_restart(event_type, mb_context, menu_manager_user, pos_id)

    show_messagebox(
        posid=pos_id,
        message="$PLEASE_WAIT_ALL_NODES_RESTART",
    )
    return mb_context.MB_SendMessage(
        destination=mb_context.hv_service,
        token=TK_HV_GLOBALRESTART,
    )


def _notify_global_restart(event_type, mb_context, menu_manager_user, pos_id):
    event_xml = '''<GLOBALRESTART posId="{}" userId="{}"/>'''
    data = event_xml.format(pos_id, menu_manager_user)
    mb_context.MB_EasyEvtSend(
        subject="GLOBAL_RESTART",
        type=event_type,
        xml=data,
        synchronous=True,
        timeout=-1,
    )

class ModelService(object):

    def __init__(
        self,
        pos_id,  # type: Any[Union[str, int]]
    ):
        # type: (...) -> None

        self.pos_id = pos_id

    def get_price_list_from_model(
        self,
        pod_type=None,    # type: Optional[str]
        sale_type=None,   # type: Optional[str]
    ):
        # type: (...) -> str

        model = get_model(posid=self.pos_id)
        if not pod_type:
            pod_type = get_podtype(model=model)

        price_list = get_pricelist(model=model)

        is_delivery = pod_type == "DL" or sale_type == "DELIVERY"
        if is_delivery and "DL" not in price_list:
            price_list = "DL." + price_list if price_list else "DL"

        special_catalog_price_list = self._get_price_list_from_special_catalog(model=model)
        if special_catalog_price_list:
            price_list = special_catalog_price_list + "." + price_list

        if "EI" not in price_list:
            price_list += ".EI"

        return price_list

    def _get_price_list_from_special_catalog(
        self,
        model,  # type: eTree
    ):
        # type: (...) -> str

        price_list = None
        special_catalog = json.loads(self.get_special_catalog(model=model))
        price_list = self._get_table_special_catalog(
            model=model,
            price_list=price_list,
            special_catalog=special_catalog,
        )
        if not price_list and "servicePeriod" in special_catalog.keys():
            price_list = special_catalog["servicePeriod"]

        return price_list

    def _get_table_special_catalog(
        self,
        model,            # type: eTree
        price_list,       # type: str
        special_catalog,  # type: dict
    ):
        # type: (...) -> Optional[str]

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

        pos_ot = sysactions.get_posot(model)
        order_id = current_order.get("orderId")
        order_picture = eTree.XML(pos_ot.orderPicture(orderid=order_id)).find("Order")
        if order_picture is None:
            return price_list

        order_state = order_picture.get("state")
        if order_state == "IN_PROGRESS" and self.is_table_service(order_picture=order_picture):
            order_properties = order_picture.find("CustomOrderProperties")
            table_id = order_properties.find("OrderProperty[@key='TABLE_ID']").get('value')
            if table_id is None:
                return price_list

            table = model.find("STORE_MODIFIED/Tables/Table[@id='" + table_id + "']")
            table_service_period = table.find("Properties").get("SERVICE_PERIOD_TABLE")
            if table_service_period is not None:
                price_list = json.loads(table_service_period).get("servicePeriod")
            else:
                sector_property = order_properties.find("OrderProperty[@key='SECTOR']")
                if special_catalog.get("sectors") is not None and sector_property is not None:
                    sector = sector_property.get('value')
                    if special_catalog["sectors"].get(sector) is not None:
                        price_list = special_catalog["sectors"][sector].get("servicePeriod")

        return price_list

    def get_special_catalog(
        self,
        model=None,  # type: Optional[eTree]
    ):
        # type: (...) -> str

        if model is None:
            model = get_model(posid=self.pos_id)

        special_catalog = get_custom(
            model=model,
            key='SERVICE_PERIOD',
        )
        if special_catalog in ['', None]:
            return '{}'

        return special_catalog

    @staticmethod
    def is_table_service(
        order_picture,  # type: eTree
    ):
        # type: (...) -> bool

        order_properties = order_picture.find("CustomOrderProperties")
        table_id = order_properties.find("OrderProperty[@key='TABLE_ID']") if order_properties is not None else None
        if table_id is not None:
            return True

        return False


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

    global MODIFIER_PREFIXES

    if MODIFIER_PREFIXES:
        return MODIFIER_PREFIXES

    MODIFIER_PREFIXES = {
        "[OnSide]": "$ONSIDE",
        "[Extra]": "$EXTRA",
        "[Light]": "$LIGHT",
        "[Additional]": "$ADDITIONAL",
        "[Without]": "$WITHOUT",
    }

    store_prefixes = sysactions.get_storewide_config(
        parameter="Modifiers.Prefixes",
        defval=[],
        array=True,
    )
    for prefix in store_prefixes:
        values = prefix.split(";") if ";" in prefix else None
        if values:
            MODIFIER_PREFIXES[values[0]] = values[1]

    return MODIFIER_PREFIXES


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

    currency_symbol = translate_message(
        model=model,
        message_key="L10N_CURRENCY_SYMBOL",
    )

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


def split_and_translate_message(
    model,      # type: str
    message,    # type: str
):
    # type: (...) -> str

    message = message[1:] if message.startswith("$") else message
    params = ""
    if "|" in message:
        message, params = message.split("|", 1)

    return translate_message(model, message, *(params.split("|")))
