import json
import logging.config
import os

from abc import ABCMeta
from multiprocessing.pool import ThreadPool
from typing import (
    Callable,
    Any,
    List,
    Union,
    Optional,
)
from xml.etree import cElementTree as eTree

from helper import config_logger, round_half_away_from_zero
from msgbus import (
    MBEasyContext, 
    TK_SYS_NAK, 
    TK_STORECFG_GET, 
    FM_PARAM, 
    TK_HV_GLOBALRESTART,
)
from persistence import Connection, Driver, DbdException
from sysactions import (
    get_model, 
    get_podtype, 
    get_pricelist, 
    get_custom, 
    get_posot, 
    show_messagebox,
)
from systools import sys_parsefilepath


class BaseRepository(object):
    __metaclass__ = ABCMeta

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

    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.mb_context,
                                 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], str, str) -> Any
        def transaction_method(conn):
            # type: (Connection) -> Any

            if conn.get_number_of_instances() != 1 and db_name is None:
                raise DbdException("Cannot open transaction without DB instance")

            conn.transaction_start()
            try:
                conn.query("BEGIN TRANSACTION")
                ret = method(conn)
                conn.query("COMMIT")
                return ret
            except Exception as _:
                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_id in pos_list:
            ret.append(self._execute_in_database(method, pos_id))

        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 execute_parallel_in_all_databases_returning_flat_list(self, method, pos_list):
        # type: (Callable[[Connection], Any], List[int]) -> Any
        inner_ret = self.execute_parallel_in_all_databases(method, pos_list)

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

        return ret

    def execute_parallel_in_all_databases(self, method, pos_list):
        # type: (Callable[[Connection], Any], List[int]) -> Any
        ret = []
        pool = ThreadPool(len(pos_list))

        for pos_id in pos_list:
            ret.append(pool.apply_async(self._execute_in_database, args=(method, pos_id)))

        pool.close()
        pool.join()

        result = [r.get() for r in ret]
        return result

    def _execute_in_database(self, method, pos_id):
        conn = None
        try:
            conn = Driver().open(self.mb_context, dbname=str(pos_id))
            return method(conn)
        finally:
            if conn is not None:
                conn.close()


def find_value_with_vars(cfg, path, default_value=None):
    value = cfg.find_value(path, default_value)
    if value is None:
        return value

    for variable_name in os.environ:
        variable_format = "{{{}}}".format(variable_name)
        if variable_format in value:
            value = value.replace(variable_format, os.environ[variable_name])
            if variable_name in ("BUNDLEDIR", "HVDATADIR"):
                value = value.replace("/./", os.sep).replace("//", os.sep).replace("/", os.sep)

    return value


def config_logger_parse_file_name(loader_path,
                                  log_name='component',
                                  configure_root=False,
                                  root_level=logging.WARN,
                                  max_files=5,
                                  max_size=5242880,
                                  add_error_handler=False):

    real_path = os.path.realpath(loader_path)
    if os.path.isfile(real_path) and real_path.endswith(".json"):
        with open(real_path, "rb") as f:
            json_loads = json.loads(f.read().decode("utf-8"))
            handlers = json_loads.get("handlers")
            if handlers:
                for handler in handlers:
                    file_name = sys_parsefilepath(handlers[handler].get("filename", ""))
                    if file_name:
                        handlers[handler]["filename"] = file_name

                loader_path = json_loads

    config_logger(loader_path, log_name, configure_root, root_level, max_files, max_size, add_error_handler)


def read_swconfig(mbcontext, key):
    rmsg = mbcontext.MB_EasySendMessage("StoreWideConfig", token=TK_STORECFG_GET, format=FM_PARAM, data=key)
    if rmsg.token == TK_SYS_NAK:
        return None

    return str(rmsg.data)


def F(value):
    if value is None:
        return 0
    return float(value)
# END F


def get_sale_line_priced_items(sale_lines, ignore_price=False):
    deleted_line_numbers = map(lambda x: x.get("lineNumber"), filter(lambda x: x.get("level") == "0" and x.get("qty") == "0", sale_lines))
    if ignore_price:
        active_sale_lines = list(filter(lambda x: (x.get("lineNumber") not in deleted_line_numbers), sale_lines))
    else:
        active_sale_lines = list(filter(lambda x: (x.get("lineNumber") not in deleted_line_numbers) and (F(x.get("itemPrice")) != 0), sale_lines))

    for sale_line in active_sale_lines:
        item_price, unit_price, item_qty, added_price, added_qty, multiplied_qty, inc_qty, level = map(sale_line.get, (("itemPrice", "unitPrice", "qty", "addedUnitPrice", "addedQty", "multipliedQty", "incQty", "level")))
        unit_price_cache = F(sale_line.get("addedUnitPrice")) or F(sale_line.get("unitPrice"))
        try:
            if F(unit_price) > 0:
                price_quantity = round_half_away_from_zero(F(item_price) / F(unit_price), 2)
                product_quantity = round_half_away_from_zero(F(item_qty) * F(multiplied_qty), 2)
                if price_quantity != product_quantity:
                    unit_price_cache = F(item_price) / product_quantity
                    unit_price = unit_price_cache
        except ZeroDivisionError:
            unit_price = 0
        try:
            correct_price = (F(unit_price) * F(item_qty) + F(added_price) * F(added_qty or item_qty)) * (F(multiplied_qty) / F(item_qty))

        except ZeroDivisionError:
            correct_price = 0

        unit_price = unit_price_cache
        if unit_price == 0 and F(item_price) > 0:
            correct_price = F(item_price)
        if not ignore_price:
            item_price = correct_price
            sale_line.set("correctItemPrice", str(item_price))

    return active_sale_lines
# END get_sale_line_priced_items


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)
    
    if 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,         # type: str
        mb_context,         # type: MBEasyContext
        menu_manager_user,  # type: str
        pos_id              # type: str
):
    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,
    )
# END restart_system


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 = 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:
                if special_catalog.get("sectors") is not None:
                    sector = order_properties.find("OrderProperty[@key='SECTOR']").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
