import math
import re

from abc import ABCMeta
from datetime import (
    datetime,
    timedelta,
)
from typing import List, Any, Callable, Tuple

from ._OrderChangerProductionBox import OrderChangerProductionBox
from production.model import ProductionOrder, Item

DATE_FORMAT = "%Y-%m-%dT%H:%M:%S"


class OrderSplitterBox(OrderChangerProductionBox):
    __metaclass__ = ABCMeta

    def __init__(
        self,
        name,
        sons,
        order_changer,
        publish_scheduler,
        split_type,
        split_value,
        logger=None
    ):
        super(OrderSplitterBox, self).__init__(name, sons, logger)
        self.order_changer = order_changer
        self.publish_scheduler = publish_scheduler
        self.split_type = split_type
        self.split_value = split_value

    def change_order(self, order):
        # type: (ProductionOrder) -> List[ProductionOrder]

        if self.split_type is None:
            return [order]
        else:
            if self.split_type.upper() == 'PREP_TIME':
                orders = self.split_prep_time(order)
            elif self.split_type.upper() == 'SALE_LINES':
                if self.split_value is None:
                    return [order]

                orders = self.split_sale_line(order, float(self.split_value))
            elif self.split_type.upper() in 'FIRE_HOLD_PRIORITY':
                orders = self.split_fire_hold_priority(order)
                self._schedule_hold_orders(orders)
                self._remove_split_tags(orders)
                self._add_fire_hold_tag(orders)
            else:
                orders = [order]

        return orders

    @staticmethod
    def split_prep_time(order):
        # type: (ProductionOrder) -> List[ProductionOrder]

        orders = []
        split_index = 0

        for item in order.items:
            for temp_order in orders:
                if len(temp_order.items) > 0 and item.prep_time == temp_order.items[0].prep_time:
                    temp_order.items.append(item)
                    break
            else:
                new_order = order.clone([item], split_index)
                orders.append(new_order)
                split_index += 1

        return orders

    def split_sale_line(self, order, limit):
        # type: (ProductionOrder, float) -> List[ProductionOrder]

        temp_items = [x for x in order.items if not self._has_fire_hold_or_priority(x)]
        if len(temp_items) < limit:
            return [order]

        orders = []
        split_index = 0
        box_limit = math.ceil(len(temp_items) / limit)
        items_per_box = math.ceil(len(temp_items) / box_limit)
        for item in temp_items:
            if len(orders) <= split_index:
                new_order = order.clone([], split_index)
                orders.append(new_order)

            orders[split_index].items.append(item)
            if len(orders[split_index].items) >= items_per_box:
                split_index += 1

        orders[0].items.extend([x for x in order.items if self._has_fire_hold_or_priority(x)])
        return orders

    def split_fire_hold_priority(self, order):
        # type: (ProductionOrder) -> List[ProductionOrder]

        orders = []
        main_order_items = []
        split_index = 1
        for item in order.items:
            if not self._has_fire_hold_or_priority(item):
                main_order_items.append(item)
                continue

            hold_item, split_index = self._handle_hold_item(
                item,
                order,
                orders,
                split_index
            )

            fire_item, split_index = self._handle_fire(
                item,
                order,
                orders,
                split_index
            )

            priority_item, split_index = self._handle_priority(
                item,
                order,
                orders,
                split_index
            )

            if not hold_item:
                if fire_item or priority_item:
                    property_list = [self._fire_in_item, self._priority_in_item]
                    clear_item = self._get_item_without_properties(item.clone(), property_list)
                    if clear_item:
                        main_order_items.append(clear_item)
            else:
                if not self._hold_in_item(item):
                    main_order_items.append(item.clone())

        self.order_changer.change_order(order.order_id, self._mark_items_as_split)

        new_order = order
        if len(orders) > 0:
            new_order = order.clone()
            new_order.order_id = str(order.order_id) + "-0"

        new_order.items = main_order_items
        orders.insert(0, new_order)

        return orders

    def _handle_hold_item(
        self,
        item,           # type: Item
        order,          # type: ProductionOrder
        orders,         # type: List[ProductionOrder]
        split_index,    # type: int
    ):
        # type: (...) -> Tuple[Any, int]

        stored_orders_states = [x for x in order.state_history if x.state == "STORED"]
        if len(stored_orders_states) == 0:
            return [], split_index

        hold_item_list = self._get_item_list_with_property(item.clone(), self._hold_in_item)
        if not hold_item_list:
            return hold_item_list, split_index

        hold_times_items = {}
        for hold_item in hold_item_list:
            hold_time = hold_item.properties.get("hold", 0)
            hold_time = int(hold_time)
            if hold_time not in hold_times_items:
                hold_times_items[hold_time] = []

            hold_times_items[hold_time].append(hold_item)

        for key in hold_times_items.keys():
            hold_type = "hold_" + str(key)
            for current_order in orders:
                if current_order.type == hold_type:
                    new_items = order.get_cloned_items(
                        allowed_items=hold_times_items[key],
                    )
                    current_order.items.extend(new_items)
                    break
            else:
                new_hold_order = order.clone(hold_times_items[key], split_index)
                new_hold_order.type = "hold_" + str(key)
                orders.append(new_hold_order)
                split_index += 1

        return hold_item_list, split_index

    def _schedule_hold_orders(self, orders):
        first_store_time = self._get_order_display_time(orders)
        if first_store_time is None:
            return

        min_hold_time = None
        filtered_orders = []
        for order in orders:
            hold_time = self._get_schedule_hold_time(order)
            if hold_time is None:
                filtered_orders.append(order)
                continue

            display_time = first_store_time + timedelta(seconds=hold_time)
            if datetime.now() > display_time:
                if not order.display_time:
                    order.display_time = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")

                for item in order.items:
                    self._remove_hold_from_items(item)

                filtered_orders.append(order)
            else:
                min_hold_time = display_time if min_hold_time is None or display_time < min_hold_time else min_hold_time

        if min_hold_time is not None:
            wait_time = min_hold_time - datetime.now()
            if wait_time.total_seconds() > 0:
                self.publish_scheduler.schedule_publish(orders[0].order_id, wait_time.seconds)

    def _remove_split_tags(self, orders):
        for order in orders:
            if "-" not in order.order_id:
                continue

            split_id = order.order_id.split("-")[1] + "-"

            for item in order.items:
                self._remove_item_split_tags(item, split_id)

    def _add_fire_hold_tag(self, orders):
        for order in orders:
            main_order = "-0" in order.order_id
            for item in order.items:
                fire_checker = self._fire_in_item if main_order else self._fire_in_item_properties
                items_with_fire = self._get_item_list_with_property(item, fire_checker)
                for fire_item in items_with_fire:
                    fire_item.add_tag("fire")

                hold_checker = self._hold_in_item if main_order else self._hold_in_item_properties
                items_with_hold = self._get_item_list_with_property(item, hold_checker)
                for hold_item in items_with_hold:
                    hold_item.add_tag("hold")

    def _remove_item_split_tags(self, item, split_id):
        regex_filter = "[0-9]-.*"
        new_tags = {}
        if item.tags:
            for tag in item.tags.keys():
                if not re.search(regex_filter, tag):
                    new_tags[tag] = tag
                elif split_id in tag:
                    new_tag = tag.replace(split_id, "")
                    new_tags[new_tag] = new_tag

        item.tags = new_tags

        for son in item.items:
            self._remove_item_split_tags(son, split_id)

    @staticmethod
    def _get_schedule_hold_time(order):
        if not hasattr(order, 'type') or "hold_" not in order.type:
            return None

        hold_time = order.type.replace("hold_", "")
        if hold_time == "-1":
            a_day_seconds = 60 * 60 * 24
            return a_day_seconds

        try:
            hold_time = int(hold_time)
            return hold_time
        except ValueError as _:
            return None

    def _remove_hold_from_items(self, item):
        if "hold" in item.properties:
            del item.properties["hold"]

        for son in item.items:
            self._remove_hold_from_items(son)

    @staticmethod
    def _get_order_display_time(orders):
        stored_orders_states = [x for x in orders[0].state_history if x.state == "STORED"]
        if not stored_orders_states:
            return None

        first_store_state = stored_orders_states[0]
        first_store_time = first_store_state.timestamp.split(".")[0]
        first_store_time = datetime.strptime(first_store_time, DATE_FORMAT)

        return first_store_time

    @staticmethod
    def _get_hold_time(item):
        try:
            hold_time = int(item.properties.get("hold"))
        except Exception as _:
            hold_time = -1

        return hold_time

    def _handle_fire(
        self,
        item,           # type: Item
        order,          # type: ProductionOrder
        orders,         # type: List[ProductionOrder]
        split_index,    # type: int
    ):
        # type: (...) -> Tuple[Any, int]

        fire_item = self._get_item_with_property(item.clone(), self._fire_in_item)
        if fire_item:
            new_fire_order = order.clone([fire_item], split_index)
            orders.append(new_fire_order)
            split_index += 1

        return fire_item, split_index

    def _handle_priority(
        self,
        item,           # type: Item
        order,          # type: ProductionOrder
        orders,         # type: List[ProductionOrder]
        split_index,    # type: int
    ):
        # type: (...) -> Tuple[Any, int]

        priority_item = self._get_item_with_property(item.clone(), self._priority_in_item)
        if priority_item:
            new_priority_order = order.clone([priority_item], split_index)
            orders.append(new_priority_order)
            split_index += 1

        return priority_item, split_index

    def _mark_items_as_split(self, root):
        # type: (Any[ProductionOrder, Item]) -> bool

        need_to_save_order = False
        child_need_to_save_order = False
        for item in root.items:
            need_to_save_order = self._mark_items_as_split(item)

            if "fire" in item.properties and "had_fire" not in item.tags:
                item.add_tag("had_fire")
                need_to_save_order = True

            if "hold" in item.properties and "had_hold" not in item.tags:
                item.add_tag("had_hold")
                need_to_save_order = True

            if "hold" not in item.properties and "had_hold" in item.tags and "clear_hold" not in item.tags:
                item.add_tag("clear_hold")
                need_to_save_order = True

            if "highPriority" in item.properties and "had_high_priority" not in item.tags:
                item.add_tag("had_high_priority")
                need_to_save_order = True

        return need_to_save_order or child_need_to_save_order

    @staticmethod
    def _hold_in_item(item):
        return "hold" in item.properties or "had_hold" in item.tags

    @staticmethod
    def _hold_in_item_properties(item):
        return "hold" in item.properties

    @staticmethod
    def _fire_in_item(item):
        return "fire" in item.properties or "had_fire" in item.tags

    @staticmethod
    def _fire_in_item_properties(item):
        return "fire" in item.properties

    @staticmethod
    def _priority_in_item(item):
        return "highPriority" in item.properties or "had_high_priority" in item.tags

    def _has_fire_hold_or_priority(self, item):
        has_fire = self._fire_in_item(item)
        has_hold = self._hold_in_item(item)
        has_high_priority = self._priority_in_item(item)

        children_has_split = False
        for child in item.items:
            children_has_split = children_has_split or self._has_fire_hold_or_priority(child)
            if children_has_split:
                break

        return has_fire or has_hold or has_high_priority or children_has_split

    def _get_item_with_property(self, item, item_has_property):
        if item_has_property(item):
            return item

        if not item.items:
            return None

        children = []
        for child in item.items:
            tag_child = self._get_item_with_property(child, item_has_property)
            if tag_child:
                children.append(tag_child)

        item.items = children
        return item if children else None

    def _get_item_without_properties(self, item, item_has_properties):
        if any([f(item) for f in item_has_properties]):
            return None

        if not item.items:
            return item

        children = []
        for child in item.items:
            tag_child = self._get_item_without_properties(child, item_has_properties)
            if tag_child:
                children.append(tag_child)

        item.items = children
        return item if children else None

    def _get_item_list_with_property(
        self,
        item,                           # type: Item
        item_has_property_function,     # type: Callable[[Item], Any]
        item_list_with_property=None,   # type: List
    ):
        # type: (...) -> List

        if item_list_with_property is None:
            item_list_with_property = []

        if item_has_property_function(item):
            item_list_with_property.append(item)

        for child in item.items:
            self._get_item_list_with_property(child, item_has_property_function, item_list_with_property)

        return item_list_with_property
