# -*- coding: utf-8 -*-
import persistence
import pyscripts
import copy
import logging
import json

from xml.etree.ElementTree import Element
from xml.etree import cElementTree as eTree
from cache import CacheManager, InfiniteExpiration
from typing import (
    List,
    Optional
)
from helper import get_valid_pricelists
from msgbus import TK_SYS_ACK
from bustoken import TK_REMOTE_ORDER_GET_DELIVERY_BRANDS

logger = logging.getLogger("PosActions")


class ModifierSets:
    def __init__(self):
        self.context = ""
        self.level = -1
        self.line_number = -1
        self.modifiers_sets = {}

    def to_xml(self):
        element = eTree.Element("ModifierSets")  # type: Element

        element.set("itemId", self.context.decode("UTF-8"))
        element.set("level", str(self.level))
        element.set("lineNumber", str(self.line_number))

        for modifier_key in self.modifiers_sets:
            element.append(self.modifiers_sets[modifier_key].to_xml())

        return element


class ModifierSet:
    def __init__(self):
        self.context = ""
        self.part_code = ""
        self.product_name = ""
        self.display_name = ""
        self.default_qty = ""
        self.min_qty = ""
        self.max_qty = ""
        self.min_item_qty = ""
        self.max_item_qty = ""
        self.json_array_tags = ""
        self.modifiers = {}

    def to_xml(self):
        element = eTree.Element("ModifierSet")  # type: Element

        element.set("defaultQty", str(self.default_qty))
        element.set("includedQty", str(self.default_qty))
        element.set("partCode", self.part_code)

        element.set("productName", self.product_name.decode("UTF-8"))
        element.set("displayName", self.display_name.decode("UTF-8"))
        element.set("jsonArrayTags", str(self.json_array_tags))

        element.set("minQty", str(self.min_qty))
        element.set("maxQty", str(self.max_qty))
        element.set("minItemQty", str(self.min_item_qty))
        element.set("maxItemQty", str(self.max_item_qty))

        for modifier_key in self.modifiers:
            element.append(self.modifiers[modifier_key].to_xml())

        return element


class Modifier:
    def __init__(self):
        self.context = ""
        self.part_code = ""
        self.product_name = ""
        self.unit_price = ""
        self.add_price = ""
        self.sub_price = ""
        self.color = ""
        self.def_qty = 0
        self.ord_qty = 0
        self.level = -1
        self.line_number = -1
        self.json_array_tags = ""
        self.parts = []

    def to_xml(self):
        element = eTree.Element("Modifier")  # type: Element

        element.set("itemId", self.context)
        element.set("partCode", self.part_code)

        element.set("productName", self.product_name.decode("UTF-8"))
        element.set("color", self.color)
        element.set("jsonArrayTags", str(self.json_array_tags))

        element.set("addedUnitPrice", str(self.add_price))
        element.set("subUnitPrice", str(self.sub_price))
        element.set("itemPrice", str(self.unit_price))

        element.set("defaultQty", str(self.def_qty))
        element.set("orderedQty", str(self.ord_qty))

        element.set("level", str(self.level))
        element.set("lineNumber", str(self.line_number))

        for part_key in self.parts:
            element.append(self.parts[part_key].to_xml())

        return element


class Part:
    def __init__(self):
        self.context = ""
        self.part_code = ""
        self.product_name = ""
        self.unit_price = ""
        self.add_price = ""
        self.sub_price = ""
        self.def_qty = 0
        self.min_qty = 0
        self.max_qty = 0
        self.level = -1
        self.line_number = -1
        self.json_array_tags = ""

    def to_xml(self):
        element = eTree.Element("Part")  # type: Element

        element.set("itemId", self.context)
        element.set("partCode", self.part_code)
        element.set("productName", self.product_name)
        element.set("jsonArrayTags", str(self.json_array_tags))

        element.set("addedUnitPrice", str(self.add_price))
        element.set("subUnitPrice", str(self.sub_price))
        element.set("itemPrice", str(self.unit_price))

        element.set("defQty", str(self.def_qty))
        element.set("minQty", str(self.min_qty))
        element.set("maxQty", str(self.max_qty))

        element.set("level", str(self.level))
        element.set("lineNumber", str(self.line_number))

        return element


class Cache:
    def __init__(self, mbcontext):
        self.mbcontext = mbcontext
        self._delivery_brands_cache = CacheManager(
            InfiniteExpiration(),
            new_object_func=self._delivery_brands_renewer
        )
        self._tags_cache = CacheManager(InfiniteExpiration(), new_object_func=self._tags_cache_renewer)
        self._parts_cache = CacheManager(InfiniteExpiration(), new_object_func=self._parts_cache_renewer)
        self._product_prices_cache = CacheManager(InfiniteExpiration(), new_object_func=self._product_prices_cache_renewer)
        self._price_key_cache = CacheManager(InfiniteExpiration(), new_object_func=self._price_key_cache_renewer)
        self._must_modify_cache = CacheManager(InfiniteExpiration(), new_object_func=self._must_modify_cache_renewer)
        self._dt_only_cache = CacheManager(InfiniteExpiration(), new_object_func=self._dt_only_cache_renewer)
        self._discount_cache = CacheManager(InfiniteExpiration(), new_object_func=self._discount_cache_renewer)
        self._options_cache = CacheManager(InfiniteExpiration(), new_object_func=self._options_cache_renewer)

    def seach_items_tree_combo(self, part_code, list_categories=[]):
        """
        Busca na arvore de um combo e retorna o ultimo item da mesma
        :param part_code: product_code ou product_part
        :return: ultimo id da arvore
        """
        tags_cache = self._tags_cache.get_cached_object()
        p_code = [part_code] if len(self.get_parts(part_code)) is 0 else filter(lambda x: x, self.get_parts(part_code))

        if len(p_code) > 1:
            list_items = []

            for code in p_code:
                if len(self.get_parts(code)) is 0:
                    tag_kiosk = tags_cache[code][0]
                    list_items.append(1 if (tag_kiosk.get('CATEGORY', None) in list_categories) else 0)
            return list_items
        else:
            if len(self.get_parts(p_code[0])) is 0:
                tag_kiosk = tags_cache[p_code[0]][0]
                return [1 if (tag_kiosk.get('CATEGORY', None) in list_categories) else 0]

        return self.seach_items_tree_combo(part_code, list_categories)
    # END of seach_items_tree_combo

    def is_not_order_kiosk(self, part_code, podtype, list_categories=[]):
        """
        Verifica se o pedido tem a tag de sobremesas para ser vendido, apenas valido para o POS 8
        :param part_code: id do produto
        :param podtype: Point-of-distribution type
        :return: Boolean
        """
        if podtype == 'KK':
            parts = [part_code] if len(self.get_parts(part_code)) is 0 else self.get_parts(part_code)
            last_items_tree = [self.seach_items_tree_combo(p_code, list_categories) for p_code in parts]
            result = list(set(sum(last_items_tree, [])))

            if not len(result) > 1:
                return True if result[0] is 0 else False
            return True
    # END of is_not_order_kiosk

    def get_tags_as_dict(self, part_code, wanted_tag):
        # type: (str, str) -> str

        tags_cache = self._tags_cache.get_cached_object()

        if wanted_tag:
            wanted_tag = wanted_tag.upper()
            if part_code in tags_cache and wanted_tag in tags_cache[part_code][0]:
                return tags_cache[part_code][0][wanted_tag]
        else:
            if part_code in tags_cache:
                return tags_cache[part_code][0]

        return ''
    # END of get_tags

    def get_tags_as_str(self, part_code):
        # type: (str) -> str

        tags_cache = self._tags_cache.get_cached_object()
        if part_code in tags_cache:
            return tags_cache[part_code][1]

        return ''
    # END of get_tags

    def get_parts(self, part_code):
        # type: (str) -> dict

        parts_cache = self._parts_cache.get_cached_object()
        if part_code in parts_cache:
            return parts_cache[part_code]

        return {}
    # END of get_parts

    def get_options(self, part_code):
        # type: (str) -> Optional[ModifierSet]
        options_cache = self._options_cache.get_cached_object()

        if part_code in options_cache:
            return copy.deepcopy(options_cache[part_code])

        return None
    # END of get_options

    def get_best_price(self, context, part_code, pod_type="EI", forced_pricelist=''):
        # type: (str, str, str, str) -> Optional[dict]

        best_price, _ = self._get_best_price_and_key(context, part_code, pod_type, forced_pricelist=forced_pricelist)
        return best_price

    def get_best_price_key(self, context, part_code, pod_type="EI", forced_pricelist=''):
        # type: (str, str, str, str) -> Optional[dict]

        _, price_key = self._get_best_price_and_key(context, part_code, pod_type, forced_pricelist=forced_pricelist)
        return price_key

    def _get_best_price_and_key(self, context, part_code, pod_type, forced_pricelist=''):
        best_price = None
        best_price_key = None

        if pod_type == "DL":
            best_price, best_price_key = self._find_best_price_and_key_by_pod_type(
                context, part_code, "DL", forced_pricelist=forced_pricelist
            )
        if best_price is None and best_price_key is None:
            best_price, best_price_key = self._find_best_price_and_key_by_pod_type(
                context, part_code, "EI", forced_pricelist=forced_pricelist
            )

        return best_price, best_price_key

    def _find_best_price_and_key_by_pod_type(self, current_context, part_code, pod_type, forced_pricelist=''):
        all_contexts = Cache._get_all_product_contexts(current_context)

        for context in all_contexts:
            price = self._get_price_in_context(context, part_code, pod_type, forced_pricelist=forced_pricelist)
            price_key = self._get_price_key_in_context(context, part_code, pod_type, forced_pricelist=forced_pricelist)
            if price is not None:
                return price, price_key
        return None, None

    @staticmethod
    def _get_all_product_contexts(current_context):
        contexts = []
        index = 0

        while index >= 0:
            contexts.append(current_context)
            index = current_context.find('.')
            current_context = current_context[index + 1:]

        contexts.append("")
        return contexts

    def _get_price_in_context(self, context, part_code, pod_type, forced_pricelist=''):
        key = (part_code, context)
        if forced_pricelist:
            pod_type = forced_pricelist
        pod_types = pod_type.split('.')[::-1]
        for price_list in pod_types:
            if price_list not in self._product_prices_cache.get_cached_object().keys():
                continue
            product_price_cache = self._product_prices_cache.get_cached_object()[price_list]
            price = product_price_cache[key] if key in product_price_cache else None
            if price:
                return price
        return None

    def _get_price_key_in_context(self, context, part_code, pod_type, forced_pricelist=''):
        key = (part_code, context)
        if forced_pricelist:
            pod_type = forced_pricelist
        pod_types = pod_type.split('.')[::-1]
        for price_list in pod_types:
            if price_list not in self._price_key_cache.get_cached_object().keys():
                continue
            product_price_key_cache = self._price_key_cache.get_cached_object()[price_list]
            price_key = product_price_key_cache[key] if key in product_price_key_cache else None
            if price_key:
                return price_key
        return None

    def get_must_modify(self, part_code, pod_type):
        must_modify_cache = self._must_modify_cache.get_cached_object()
        dt_only_cache = self._dt_only_cache.get_cached_object()

        if part_code in must_modify_cache:
            if pod_type != "DT" and part_code in dt_only_cache:
                return None
            else:
                return must_modify_cache[part_code]

        return None
    # END get_best_price_key

    def get_discount(self, part_code, discount_code):
        discount_cache = self._discount_cache.get_cached_object()
        key = (part_code, discount_code)
        if key in discount_cache:
            return discount_cache[key]

        return None
    # END get_best_price_key

    def get_delivery_brands(self):
        # type: () -> List

        return self._delivery_brands_cache.get_cached_object()

    def _delivery_brands_renewer(self):
        # type: () -> List

        try:
            result = self.mbcontext.MB_EasySendMessage(
                dest_name="RemoteOrder",
                token=TK_REMOTE_ORDER_GET_DELIVERY_BRANDS,
                data=""
            )
            if result.token == TK_SYS_ACK:
                return json.loads(result.data)

            return []

        except Exception as _:
            return []

    def _tags_cache_renewer(self):
        conn = None
        try:
            conn = persistence.Driver().open(self.mbcontext)
            cursor = conn.select("""SELECT ProductCode, upper(Tag) FROM productdb.ProductTags ORDER BY ProductCode""")
            temp_cache = [(x.get_entry(0), x.get_entry(1)) for x in cursor]

            if not temp_cache:
                return {}

            tags_cache = {}
            tags = {}
            tag_str = ""
            current_code = temp_cache[0][0]
            for tags_line in temp_cache:
                if current_code != tags_line[0]:
                    tags_cache[current_code] = (tags, tag_str)
                    current_code = tags_line[0]
                    tags = {}
                    tag_str = ""

                tmp = tags_line[1].split('=')
                tags[tmp[0].upper()] = tmp[1] if len(tmp) > 1 else ''
                tag_str = tag_str + tags_line[1]

            tags_cache[current_code] = (tags, tag_str)
            return tags_cache
        except (persistence.AprException, persistence.DbdException) as _:
            logger.exception('Error getting tags to renew cache')
            return {}
        finally:
            if conn:
                conn.close()

    def _parts_cache_renewer(self):
        conn = None
        try:
            conn = persistence.Driver().open(self.mbcontext)
            query = """SELECT  ProductPart.ProductCode, 
                                ProductPart.PartCode, 
                                DefaultQty, 
                                MinQty, 
                                MaxQty, 
                                ProductName 
                         FROM ProductPart 
                        INNER JOIN Product ON ProductPart.PartCode = Product.ProductCode 
                        WHERE PartType != 1 
                        ORDER BY ProductPart.ProductCode"""
            cursor = conn.select(query)
            temp_cache = [
                (
                    x.get_entry(0),
                    x.get_entry(1),
                    (
                        x.get_entry(2),
                        x.get_entry(3),
                        x.get_entry(4),
                        x.get_entry(5)
                     )
                ) for x in cursor
            ]

            if not temp_cache:
                return {}

            parts_cache = {}
            parts = {}
            current_code = temp_cache[0][0]
            for part_line in temp_cache:
                if current_code != part_line[0]:
                    parts_cache[current_code] = parts
                    current_code = part_line[0]
                    parts = {}

                parts[part_line[1]] = part_line[2]

            parts_cache[current_code] = parts
            return parts_cache
        except (persistence.AprException, persistence.DbdException) as _:
            logger.exception('Error getting parts to renew cache')
            return {}
        finally:
            if conn:
                conn.close()

    def _product_prices_cache_renewer(self):
        conn = None
        try:
            lists = get_valid_pricelists(self.mbcontext)
            all_prices = {}
            conn = persistence.Driver().open(self.mbcontext)
            for price_list in lists:
                all_prices[price_list] = Cache._query_all_prices_by_price_list(conn, price_list)

            return all_prices
        finally:
            if conn:
                conn.close()

    @staticmethod
    def _query_all_prices_by_price_list(conn, price_list):
        query_all_price_list = """
            SELECT productcode, 
                   context, 
                   defaultunitprice, 
                   addedunitprice, 
                   subtractedunitprice, 
                   pricelistid 
            FROM   price 
            WHERE  validthru >= Datetime(Datetime(), 'localtime') 
                   AND validfrom <= Datetime(Datetime(), 'localtime') 
                   AND pricelistid = '{}' """

        return {(str(x.get_entry(0)), str(x.get_entry(1) or '')):
                (x.get_entry(2) or '0.00', x.get_entry(3) or '0.00', x.get_entry(4) or 0)
                for x in conn.select(query_all_price_list.format(price_list))}

    def _price_key_cache_renewer(self):
        conn = None
        try:
            lists = get_valid_pricelists(self.mbcontext)
            all_prices_key = {}
            conn = persistence.Driver().open(self.mbcontext)
            for price_list in lists:
                all_prices_key[price_list] = Cache._query_all_price_keys_by_price_list(conn, price_list)

            return all_prices_key
        finally:
            if conn:
                conn.close()

    @staticmethod
    def _query_all_price_keys_by_price_list(conn, price_list):
        query_all_price_keys_by_price_list = """
            SELECT productcode, 
                   context, 
                   pricekey, 
                   pricelistid 
            FROM   price 
            WHERE  validthru >= Datetime(Datetime(), 'localtime') 
                   AND validfrom <= Datetime(Datetime(), 'localtime') 
                   AND pricelistid = '{}' """

        return {(str(x.get_entry(0)), str(x.get_entry(1) or '')): x.get_entry(2)
                for x in conn.select(query_all_price_keys_by_price_list.format(price_list))}

    def _must_modify_cache_renewer(self):
        conn = None
        try:
            conn = persistence.Driver().open(self.mbcontext)
            return {str(x.get_entry(0)): str(x.get_entry(1)).split("=")[1] for x in conn.select(
                """SELECT ProductCode, Tag from ProductTags where Tag like 'MustBeModifiedSet%' """)}
        finally:
            if conn:
                conn.close()

    def _dt_only_cache_renewer(self):
        conn = None
        try:
            conn = persistence.Driver().open(self.mbcontext)
            return {str(x.get_entry(0)): str(x.get_entry(1)).split("=")[1] for x in
                    conn.select("""SELECT ProductCode, Tag from ProductTags where Tag like 'DTOnly%' """)}
        finally:
            if conn:
                conn.close()

    def _discount_cache_renewer(self):
        conn = None
        try:
            conn = persistence.Driver().open(self.mbcontext)
            return {(str(x.get_entry(0)), str(x.get_entry(1))): (x.get_entry(3), x.get_entry(4)) for x
                    in conn.select("""SELECT * from ProductDiscounts where Exclude == 'N'""")}
        finally:
            if conn:
                conn.close()

    def _options_cache_renewer(self):
        conn = None
        try:
            conn = persistence.Driver().open(self.mbcontext)
            query_result = [(str(x.get_entry(0)), str(x.get_entry(1)), str(x.get_entry(2)), str(x.get_entry(3)),
                             str(x.get_entry(4)), str(x.get_entry(5))) for x in conn.select("""
            select pc.ClassCode, p.ProductName, pt.Tag, pc.ProductCode, p2.ProductName, pt2.Tag from ProductClassification pc
            inner join Product p
            on pc.ClassCode = p.ProductCode
            left outer join ProductTags pt
            on p.ProductCode = pt.ProductCode
            inner join Product p2
            on p2.ProductCode = pc.ProductCode
            left outer join ProductTags pt2
            on p2.ProductCode = pt2.ProductCode
            where (pt.Tag like 'DisplayName%%' or pt.Tag like 'ItemQtys%%' or pt2.Tag like 'Color%%')
            and ClassCode <> 1
            order by pc.ClassCode, p.ProductCode""")]

            if not query_result:
                return

            new_option_cache = {}

            current_modifier_set = ModifierSet()
            current_modifier_set.part_code = query_result[0][0]
            current_modifier_set.product_name = query_result[0][1]
            current_modifier_set.default_qty = 1
            current_modifier_set.min_qty = 1
            current_modifier_set.max_qty = 1
            current_modifier_set.min_item_qty = 0
            current_modifier_set.max_item_qty = 1
            if query_result[0][2].upper().startswith("DISPLAYNAME"):
                current_modifier_set.display_name = query_result[0][2].split("=")[1]
            elif query_result[0][2].upper().startswith("ITEMQTYS"):
                item_qtys = query_result[0][2].split("=")[1]
                current_modifier_set.min_item_qty = item_qtys.split(";")[0]
                current_modifier_set.max_item_qty = item_qtys.split(";")[1]

            products_processed = {}
            modifiers = {}
            for query_line in query_result:
                if current_modifier_set.part_code != query_line[0]:
                    current_modifier_set.modifiers = modifiers
                    new_option_cache[current_modifier_set.part_code] = current_modifier_set

                    current_modifier_set = ModifierSet()
                    current_modifier_set.part_code = query_line[0]
                    current_modifier_set.product_name = query_line[1]
                    current_modifier_set.default_qty = 1
                    current_modifier_set.min_qty = 1
                    current_modifier_set.max_qty = 1
                    current_modifier_set.min_item_qty = 0
                    current_modifier_set.max_item_qty = 1

                    modifiers = {}
                    products_processed = {}

                if query_line[2].upper().startswith("DISPLAYNAME"):
                    current_modifier_set.display_name = query_line[2].split("=")[1]
                elif query_line[2].upper().startswith("ITEMQTYS"):
                    item_qtys = query_line[2].split("=")[1]
                    current_modifier_set.min_item_qty = item_qtys.split(";")[0]
                    current_modifier_set.max_item_qty = item_qtys.split(";")[1]

                if query_line[3] not in products_processed:
                    mod = Modifier()
                    mod.part_code = query_line[3]
                    mod.product_name = query_line[4]
                    if query_line[5].upper().startswith("COLOR"):
                        mod.color = query_line[5].split("=")[1]

                    modifiers[mod.part_code] = mod
                    products_processed[mod.part_code] = mod
                else:
                    if query_line[5].upper().startswith("COLOR"):
                        mod = products_processed[query_line[3]]
                        mod.color = query_line[5].split("=")[1]

            current_modifier_set.modifiers = modifiers
            new_option_cache[current_modifier_set.part_code] = current_modifier_set

            return new_option_cache
        finally:
            if conn:
                conn.close()


cache = Cache(pyscripts.mbcontext)
