import json
import re
import xml.sax.handler

from cache_functions import UsersInfoCache
from typing import (
    Dict,
    Optional,
)

from production.model import ProductionOrder, Item, Comment, StateEvent


def xml_str(v):
    return str(v) if v is not None else None


def xml_bool(v):
    return "true" if (v and str(v).lower() == "true") else "false"


def json_object(value):
    try:
        return json.loads(value)
    except (Exception,):
        return None


class SystemOrderParser(xml.sax.handler.ContentHandler):
    # [production prop], [<Order> attr], [type converter]
    # <Order> attributes to be merged
    _ORDER_MERGE = (
        ('state', 'state', str),
        ('state_id', 'stateId', int),
        ('originator', 'originatorId', str),
        ('originator_number', 'originatorNumber', int),
        ('pos_id', 'posId', int),
        ('pod_type', 'podType', str),
        ('order_type', 'type', str),
        ('order_subtype', 'subType', str),
        ('sale_type', 'saleTypeDescr', str),
        ('total_gross', 'totalGross', str),
        ('tax_total', 'taxTotal', str),
    )
    # ALL <Order> attributes
    _ORDER_ATTRS = _ORDER_MERGE + (
        ('order_id', 'orderId', str),
        ('period', 'businessPeriod', int),
        ('pod_type', 'podType', str),
        ('created_at', 'createdAt', str),
        ('created_at_gmt', 'createdAtGMT', str),
        ('business_day', 'businessPeriod', str),
        ('operator_id', 'operatorId', int),
        ('session_id', 'sessionId', str),
        ('selected_pickup_area', 'pickupArea', str),
    )
    # <SaleLine> attributes to be merged
    _LINE_MERGE = (
        ('qty', 'qty', float),
        ('added_qty', 'addedQty', float),
        ('sub_qty', 'subQty', float),
        ('multiplied_qty', 'multipliedQty', float),
        ('modifier_label', 'modifierLabel', xml_str),
        ('only', 'onlyModifiers', xml_bool),
        ('properties', 'customProperties', json_object),
    )
    # noinspection PyTypeChecker
    # ALL <SaleLine> attributes
    _LINE_ATTRS = _LINE_MERGE + (
        ('line_number', 'lineNumber', int),
        ('item_id', 'itemId', str),
        ('part_code', 'partCode', int),
        ('description', 'productName', unicode),  # noqa: F821
        ('item_type', 'itemType', str),
        ('level', 'level', float),
        ('pos_level', 'level', float),
        ('default_qty', 'defaultQty', float),
        ('min_qty', 'minQty', float),
        ('max_qty', 'maxQty', float),
        ('last_order_qty', 'lastOrderedQty', float),
        ('product_priority', 'productPriority', int),
        ('json_tags', 'jsonArrayTags', str),
        ('last_updated', 'lastUpdated', str),
        ('measure_unit', 'measureUnit', str),
        ('created_at', 'createdAt', str)
    )
    # <-- ALL <State> attributes
    _STATE_ATTRS = (
        ('state', 'state', str),
        ('state_id', 'stateId', int),
        ('timestamp', 'timestamp', str),
    )

    current_item = None

    def __init__(
        self,                           # type: SystemOrderParser
        part_code_jit_lines,            # type: Dict[int, str]
        product_custom_params,          # type: Dict[str, Dict[int, str]]
        merge_existing=True,            # type: Optional[bool]
        user_cache_thread=None,         # type: Optional[UsersInfoCache]
    ):
        # type: (...) -> None

        self.part_code_jit_lines = part_code_jit_lines
        self.product_custom_params = product_custom_params
        self.merge = merge_existing
        self.user_cache_thread = user_cache_thread

        self.order = None                       #: Contains the resulting order (after the parsing is done)
        self.saved_order = None                 #: Contains the original order (for comparing after parsing)
        self._stack = {}                        #: hierarchical pile of items
        self.current_item = None                #: item being parsed
        self.mainItem = None                    #: level 0 item

        if merge_existing:
            self._missing = set()

    @staticmethod
    def _create_attributes(tuples, attrs):
        d = {}
        for my_name, other_name, func in tuples:
            value = attrs.get(other_name)
            if value or hasattr(func, 'allowempty'):
                d[my_name] = func(value)
        return d

    def startElement(self, name, attrs):
        if name == "Order":
            self._parse_order(attrs)
        elif name == "SaleLine":
            self._parse_line(attrs)
        elif name == "State":
            self._parse_state(attrs)
        elif name == "OrderProperty":
            self._parse_property(attrs)
        elif name == "Comment":
            self._parse_comment(attrs)

    def endElement(self, name):
        if self.merge and name == "Order" and self._missing:
            # After all the order has been merged, void items that where not found
            for item in self._missing:
                if item.qty > 0:
                    item.qty = 0
        if self.saved_order is not None:
            # Check if the order has changed
            self.order.modified = self.order.equal_items(self.saved_order) is False

        # Check voided items
        for item in self.order.items:
            if item.qty == 0:
                self._mark_voided(item)
            elif item.voided is True:
                self._clear_voided(item)

    def endDocument(self):
        self._merge_fraction_lines()

    def _mark_voided(self, item):
        item.voided = True
        for sub_item in item.items:
            self._mark_voided(sub_item)

    def _clear_voided(self, item):
        item.voided = False
        for sub_item in item.items:
            self._clear_voided(sub_item)

    def _parse_order(self, attrs):
        if self.order is None:  # Create a new order
            kargs = self._create_attributes(self._ORDER_ATTRS, attrs)
            self.order = ProductionOrder(**kargs)
            self._update_operator_info()
        else:  # Merge attributes
            self.saved_order = self.order.clone()
            order = self.order
            for key, value in self._create_attributes(self._ORDER_MERGE, attrs).items():
                setattr(order, key, value)
            # Populate the list of items, so that we can find "missing ones" later
            self._missing = set(order.items)

    def _merge_fraction_lines(
        self,   # type: SystemOrderParser
    ):
        # type: (...) -> None

        merged_items = []
        for item in self.order.items:
            merged_items.append(item)
            if item.level != 0 or "OMGR_FRACTION_FROM_LINE" not in item.properties:
                continue

            original_line_number = int(item.properties.get("OMGR_FRACTION_FROM_LINE"))
            if item.line_number == original_line_number:
                fractionated_by = int(item.properties.get("fractionatedBy", 1))
                for from_item in merged_items:
                    from_item.multiplied_qty = item.multiplied_qty * fractionated_by

                continue

            for from_item in merged_items:
                if from_item.line_number != original_line_number:
                    continue

                from_item.qty += item.qty
                self._set_item_multiplied_qty(
                    from_item=from_item,
                    original_item=item,
                )

                break

            merged_items.remove(item)

        self.order.items = merged_items

    def _set_item_multiplied_qty(
        self,           # type: SystemOrderParser
        from_item,      # type: Item
        original_item,  # type: Item
    ):
        # type: (...) -> None

        from_item.multiplied_qty += (original_item.multiplied_qty * from_item.qty)
        for child in from_item.items:
            self._set_item_multiplied_qty(
                from_item=child,
                original_item=original_item,
            )

    def _update_operator_info(self):
        self._update_operator_id()

        users = self.user_cache_thread.get_users_info() if self.user_cache_thread else []
        if self.order.operator_id in users:
            user = users[self.order.operator_id]
            self._update_operator_name(user.name)
            self._update_operator_level(user.level)

    def _update_operator_id(self):
        self.order.operator_id = self._parse_user_from_session_id() if self._session_id_has_user() else "0"

    def _update_operator_name(self, name):
        self.order.operator_name = name

    def _update_operator_level(self, level):
        self.order.operator_level = level

    def _parse_user_from_session_id(self):
        return self.order.session_id.split("user=")[1].split(",")[0]

    def _session_id_has_user(self):
        return "user=" in self.order.session_id

    def _parse_line(self, attrs):
        item = self._find_item(attrs) if self.merge else None
        if item is None:
            # Create a new item
            kargs = self._create_attributes(self._LINE_ATTRS, attrs)
            item = Item(**kargs)

            if item.last_updated:
                item.last_updated += "Z"

            item_course = 'default'
            if item.properties is not None:
                if "fractionatedBy" in item.properties:
                    fractionated_by = item.properties["fractionatedBy"]
                    item.fractionated_by = fractionated_by

                if "seat" in item.properties:
                    seat = item.properties["seat"]
                    item.seat = seat if seat != "0" else ''

                if "courseAbbreviation" in item.properties:
                    course = item.properties["courseAbbreviation"]
                    item.manual_course = course

                if "course" in item.properties:
                    item_course = (item.properties["course"]).lower()

                if "fire" in item.properties:
                    fire = item.properties["fire"].lower() == "true"
                    item.fire = fire

                if "highPriority" in item.properties:
                    high_priority = item.properties["highPriority"].lower() == "true"
                    item.high_priority = high_priority

                try:
                    if "sequence" in item.properties:
                        sequence = json.loads(item.properties["sequence"])
                        item.sequence = int(sequence["kitchenSequence"]) if "kitchenSequence" in sequence else 1000
                except (TypeError, ValueError):
                    item.sequence = 1000

                if "fraction" in item.properties:
                    item.fraction = int(item.properties["fraction"])

            jit_lines = self.part_code_jit_lines.get(item.part_code, "")
            item.jit_lines = self.get_part_code_jit_by_course(jit_lines.replace(" ", ""), item_course)

            for custom_param_id in self.product_custom_params.keys():
                if item.part_code in self.product_custom_params[custom_param_id]:
                    item.custom_params = {
                        custom_param_id: self.product_custom_params[custom_param_id].get(item.part_code)
                    }

            # Put it on its parent list
            level = float(item.level)
            parent = self._get_parent(level)
            if parent:
                parent.items.append(item)
        else:  # Just merge the attributes
            for key, value in self._create_attributes(self._LINE_MERGE, attrs).items():
                setattr(item, key, value)

        if item.level == 0:
            self.mainItem = item
        elif item.level > 0:
            item.main_item_description = self.mainItem.description

        # Record where we are
        self._stack[str(item.level)] = item
        self.current_item = item

    @staticmethod
    def get_part_code_jit_by_course(jit_lines, item_course):
        part_code_jit = jit_lines.strip()
        part_code_jit_list = part_code_jit.split(';')
        jit_dict = {}

        for jit_item in part_code_jit_list:
            try:
                course = re.findall('{(.*)}', jit_item)[0]
            except IndexError:
                course = "default"

            jit_dict[course.lower()] = jit_item.replace("{" + course + "}", "")

        return jit_dict[item_course] if item_course in jit_dict else jit_dict['default']

    def _parse_comment(self, attrs):
        description = attrs.get("comment")
        if description is not None:
            comment = Comment()
            comment.comment_id = attrs.get("commentId")
            comment.text = description
            self.current_item.comments[comment.comment_id] = comment

    def _parse_property(self, attrs):
        ignore_keys = [
            "FISCAL_XML",
            "CANCELED_FISCAL_XML",
            "DISABLED_FISCAL_XML",
            "FISCAL_ID",
            "FISCALIZATION_DATE",
        ]

        key = attrs.get('key')
        if key in ignore_keys:
            return
        value = attrs.get('value')
        if value and self.order:
            self.order.properties[key] = value

    def _find_item(self, attrs):
        level = float(attrs.get("level"))
        line = int(attrs.get("lineNumber"))
        item_code = "%s.%s" % (attrs.get("itemId"), attrs.get("partCode"))

        parent = self._get_parent(level)
        if not parent:
            return None
        for item in parent.items:
            if (item.line_number == line) and (item.item_code == item_code) and (item.level == level):
                self._missing.discard(item)
                return item
        return None  # Not found

    def _get_parent(self, level):
        if level == 0:
            return self.order
        return self._stack.get(str(level - 1), None) or self._stack.get(str(float(int(level) - 1)), None)

    def _parse_state(self, attrs):
        kargs = self._create_attributes(self._STATE_ATTRS, attrs)
        new = StateEvent(**kargs)
        for event in self.order.state_history:
            if event.state_id == new.state_id and event.timestamp == new.timestamp:
                break  # This event is already in the order
        else:
            # Event not found... add it!
            self.order.state_history.append(new)
