import cPickle as Pickle
from datetime import datetime

import pytz
from typing import List, Optional
from tzlocal import get_localzone
from ._CustomStorage import CustomStorage
from ._Item import Item
from ._ProdStates import ProdStates
from ._StateEvent import StateEvent
from ._TagEvent import TagEvent

time_offset = datetime.now() - datetime.utcnow()


class ProductionOrder(CustomStorage):

    """ class ProductionOrder(CustomStorage)
    Represents a production order.
    Instances of this class can be handled and/or modified by any production-box.
    Use the methods write_data, read_data and delete_data in order to modify instances
    in a safe way (avoiding name conflicts).
    """
    # Note - NEVER use mutable objects as the default value here (they are shared by all instances)
    # Mutable objects MUST be created in the constructor
    order_id = 0  #: Order identification number
    state = ""  #: Current order state ("IN_PROGRESS", "TOTALED", ..etc..)
    state_id = 0  #: Current state number
    prod_sequence = ""  #: Production sequence number used to sort orders - SHOULD ONLY BE SET ON A CLONED ORDER
    pos_id = 0  #: Current order POS id
    originator = ""  #: Name of the order originator
    pod_type = ""  #: The point-of-distribution of this order ("FC", "DT")
    created_at = ""  #: Local timestamp of the "order start" (in database format: YYYY-MM-DDTHH:MM:SS[.SSS])
    business_day = ""
    created_at_gmt = ""  #: UTC timestamp of the "order start" (in database format: YYYY-MM-DDTHH:MM:SS[.SSS])
    operator_id = 0  #: ID of the operator who took this order
    operator_name = ""
    operator_level = 0
    order_type = ""  #: Order type ("SALE", "REFUND", ..etc..)
    order_subtype = ""  #: Order subtype (script-defined, and generally not known by the kernel)
    sale_type = ""  #: Current sale type (EAT_IN, TAKE_OUT, etc...)
    total_gross = ""  #: Order total gross amount
    product_priority = 0  #: Product display priority
    state_history = None  #: List of StateEvent objects of this order
    items = None  #: Level-0 items of this order
    properties = None  #: List of OrderProperties from the Order Picture
    cloned = False  #: Indicates if this is a cloned instance of an order
    purged = False  #: Indicates if this order has just been purged from the database
    session_id = ""  #: Session ID for this order
    display_time = ""  #: Time to be displayed in the KDS
    tagged_timestamp = None  #: Time when the whole order was tagged
    recalled = False  #: Whether this order was called back after being served
    first_processing = False  #: Indicates if this is the first processing time
    max_prep_time = None
    cost = None
    skip_course = False
    round_robin_path = ""
    tags = None
    current_course = ''
    jit_printer_name = ''
    order_id_count = ''
    _prod_state_history = None
    selected_pickup_area = None
    stored_split_order = False

    def __init__(self, **kargs):
        super(ProductionOrder, self).__init__()
        self.state_history = []  # type: List[StateEvent]
        self.items = []  # type: List[Item]
        self.properties = {}
        self.undo_list = []  # type: List[ChangeProdStateUndoCommand]
        self.tag_history = []  # type: List[TagEvent]
        self.cost = None
        self._prod_state_history = []  # type: List[StateEvent]
        self.tags = {}
        self.selected_pickup_area = {}
        self.showed_lines = {}
        for key, val in kargs.items():
            setattr(self, key, val)

    @property
    def prod_state(self):
        if self._prod_state_history:
            return self._prod_state_history[-1].prod_state
        return ProdStates.NORMAL

    @prod_state.setter
    def prod_state(self, new_state):
        if self._prod_state_history and new_state == self._prod_state_history[-1].prod_state:
            return

        date_now = datetime.now()
        timezone = pytz.timezone(get_localzone().zone)
        date_now_tz = timezone.localize(date_now).strftime("%Y-%m-%dT%H:%M:%S%z")
        self._prod_state_history.append(StateEvent(prod_state=new_state, timestamp=date_now_tz))

    @property
    def prod_state_last_update(self):
        if self._prod_state_history:
            return self._prod_state_history[-1].timestamp
        return None

    @property
    def prod_state_history(self):
        return tuple(self._prod_state_history or [])

    @property
    def display_time_gmt(self):
        try:
            timestamp_date = datetime.strptime(self.display_time[0:19], "%Y-%m-%dT%H:%M:%S")
            gmt_date = timestamp_date - time_offset
            return gmt_date.isoformat() + "Z"
        except ValueError:
            return ""

    def __str__(self):
        ret = ""
        for item in self.items:
            ret += str(item) + "\n"

        return "Id: {}, ProdState: {}, Sequence: {}, SaleType: {}, State: {}{}{}{}\nItems:\n{}".format(
            self.order_id,
            self.prod_state,
            self.prod_sequence,
            self.sale_type,
            self.state,
            ", MaxPrepTime: {}".format(self.max_prep_time) if self.max_prep_time else "",
            ", Cost: {}".format(self.cost) if self.cost is not None else "",
            ", RoundRobinPath: {}".format(self.round_robin_path) if hasattr(self, "round_robin_path") else "",
            ret
        )

    def __repr__(self):
        return "(%s) %s" % (object.__repr__(self), str(self))

    def __eq__(self, other):
        # type: (ProductionOrder) -> bool
        prop_list_to_compare = [
            "order_id",
            "state_history",
            "items",
            "prod_state",
            "tags",
            "display_time",
            "properties",
            "selected_pickup_area",
        ]

        def _dump_prop(order, prop):
            return Pickle.dumps(getattr(order, prop), Pickle.HIGHEST_PROTOCOL)

        if not other:
            return False

        if not isinstance(other, ProductionOrder):
            return NotImplemented

        return not any(_dump_prop(other, p) != _dump_prop(self, p) for p in prop_list_to_compare)

    def add_tag(self, tag):
        if tag not in self.tags:
            date_now = datetime.now()
            timezone = pytz.timezone(get_localzone().zone)
            date_now_tz = timezone.localize(date_now).strftime("%Y-%m-%dT%H:%M:%S%z")

            self.tags[tag] = date_now_tz

    def remove_tag(self, tag):
        if tag in self.tags:
            del self.tags[tag]

    def add_line_tag(self, line_id, tag, line_jit_list=None):
        item = self.get_item_by_line_id(line_id)
        if item is None:
            return

        item.add_tag(tag, line_jit_list)
        if item.is_option():
            self._add_line_sons_tags(item, tag, line_jit_list)

        self.build_showed_lines()

    def _add_line_sons_tags(self, item, tag, line_jit_list=None):
        for son in item.items:
            son.add_tag(tag, line_jit_list)
            if son.items:
                self._add_line_sons_tags(son, tag, line_jit_list)

    def remove_line_tag(self, line_id, tag, line_jit_list=None):
        item = self.get_item_by_line_id(line_id)
        if item is None:
            return

        item.remove_tag(tag, line_jit_list)
        if item.is_option():
            self._remove_line_sons_tags(item, tag, line_jit_list)

        self.build_showed_lines()

    def _remove_line_sons_tags(self, item, tag, line_jit_list=None):
        for son in item.items:
            son.remove_tag(tag, line_jit_list)
            if son.items:
                self._remove_line_sons_tags(son, tag, line_jit_list)

    def toggle_line_tag(self, line_id, tag, line_jit_list=None):
        item = self.get_item_by_line_id(line_id)
        if item is None:
            return

        jit_tags = item.toggle_tag(tag, line_jit_list)
        for jit_tag in jit_tags:
            if item.has_tag(jit_tag):
                self._add_line_sons_tags(item, tag, line_jit_list)
            else:
                self._remove_line_sons_tags(item, tag, line_jit_list)

    def build_showed_lines(self):
        self.showed_lines = {}
        count = 1
        for item in self.items:
            count = self.build_item_tags(item, count)

    def build_item_tags(self, item, count):
        if item.is_ingredient():
            return count

        has_show = False
        for tag in item.get_tags():
            if tag.startswith("show"):
                has_show = True
                break

        line_id = item.get_line_id()
        if has_show:
            if line_id not in self.showed_lines:
                if item.measure_unit == "UN":
                    qty = int(item.multiplied_qty)
                else:
                    qty = 1

                self.showed_lines[line_id] = list(range(count, count + qty))
                count += qty
        else:
            if line_id in self.showed_lines:
                del self.showed_lines[line_id]

        for son in item.items:
            count = self.build_item_tags(son, count)

        return count

    def get_item_by_line_id(
        self,           # type: ProductionOrder
        line_id,        # type: str
    ):
        # type: (...) -> Optional[Item]

        for item in self.items:
            found = self.get_item_or_son(line_id, item)
            if found is not None:
                return found

            for joined_item in item.joined_items:
                found = self.get_item_or_son(line_id, joined_item)
                if found is not None:
                    return found

        return None

    def get_item_or_son(self, line_id, item):
        if item.get_line_id() == line_id:
            return item

        for son in item.items:
            found = self.get_item_or_son(line_id, son)
            if found is not None:
                return found

        return None

    def get_item(self, item):
        # type: (Item) -> Optional[Item]

        for current_item in self.items:
            if current_item.line_number == item.line_number and current_item.item_code == item.item_code:
                return current_item
        return None

    def clone(self, items=None, index=None):
        # type: (Optional[List[Item]], Optional[int]) -> ProductionOrder

        cloned_order = Pickle.loads(Pickle.dumps(self, -1))
        if items is not None:
            cloned_order.items = Pickle.loads(Pickle.dumps(items, -1))

        cloned_order.order_id = str(self.order_id) + "-" + str(index) if index is not None else self.order_id
        return cloned_order

    def get_cloned_items(
        self,               # type: ProductionOrder
        allowed_items,      # type: List[Item]
    ):
        # type: (...) -> List[Item]

        cloned_items = Pickle.loads(Pickle.dumps(self.items, -1))
        items_ids = [x.get_line_id() for x in allowed_items]

        items = self._get_items(
            items=cloned_items,
            allowed_items_id=items_ids,
            include_all_sons_from_allowed_items=True,
        )

        return items

    def _get_items(
        self,                                   # type: ProductionOrder
        items,                                  # type: List[Item]
        allowed_items_id,                       # type: List[str]
        include_all_sons_from_allowed_items,    # type: bool
    ):
        if not items:
            return []

        new_items = []
        for item in items:
            item_allowed = item.get_line_id() in allowed_items_id
            if item_allowed and include_all_sons_from_allowed_items:
                sons_items = item.items
            else:
                sons_items = self._get_items(
                    items=item.items,
                    allowed_items_id=allowed_items_id,
                    include_all_sons_from_allowed_items=include_all_sons_from_allowed_items,
                )

            if item_allowed or sons_items:
                item.items = sons_items
                new_items.append(item)

        return new_items

    def equal_items(self, order):
        if order is None:
            return False

        if isinstance(order, ProductionOrder) is False:
            return False

        for item in order.items:
            saved_item = self.get_item(item)
            if saved_item is None:
                return False
            if compare_items(item, saved_item, is_level0=True) is False:
                return False

        for item in self.items:
            other_item = order.get_item(item)
            if other_item is None:
                return False
            if compare_items(item, other_item, is_level0=True) is False:
                return False
        return True


def compare_items(item1, item2, is_level0=False):
    if item1 is None or item2 is None:
        return False

    items1, items2 = real_items(item1.items), real_items(item2.items)
    if (item1.part_code == item2.part_code) and (len(items1) == len(items2)) and (item1.level == item2.level):
        if (not is_level0) and item1.qty != item2.qty:
            return False  # Only consider the quantity for non-level0
        if compare_comments(item1, item2) is False:
            # Items have different comments
            return False
        if items1 and not all(map(compare_items, items1, items2)):
            return False
        return True


def compare_comments(item1, item2):
    if item1 is None or item2 is None:
        return False

    none_comments = bool(item1.comments is None) + bool(item2.comments is None)
    if none_comments == 0:
        return True

    if none_comments == 1:
        return False

    if item1.comments.keys() != item2.comments.keys():
        return False

    for key in item1.comments:
        item1_comment = item1.comments[key]
        item2_comment = item2.comments[key]
        if item1_comment.comment_id != item2_comment.comment_id:
            return False
        if item1_comment.text != item2_comment.text:
            return False

    return True


def real_items(item_list):
    def _real_item(item):
        type_ok = (item.item_type not in ("INGREDIENT", "CANADD"))
        return type_ok or (item.default_qty != item.qty) or (item.only == "true")

    return [x for x in item_list if _real_item(x)]
