import re

from datetime import (
    datetime,
    timedelta,
)
from logging import Logger

from typing import (
    List,
    Any,
    Callable,
    Tuple,
    Optional,
)

from production.model import (
    Item,
    ProductionOrder, OrderChanger,
)

from ._SplitterUtil import (
    must_stay_in_main_order,
    fire_in_item,
    priority_in_item,
    hold_in_item,
    clear_fire,
    clear_priority,
    clear_hold,
)
from .._OrderChangerProductionBox import OrderChangerProductionBox
from ...manager import (
    PublishScheduler,
)


class FireHoldPriorityOrderSplitter(OrderChangerProductionBox):
    def __init__(
        self,                   # type: FireHoldPriorityOrderSplitter
        name,                   # type: str
        sons,                   # type: object
        order_changer,          # type: OrderChanger
        publish_scheduler,      # type: PublishScheduler
        logger=None,            # type: Optional[Logger]
    ):
        # type: (...) -> None

        super(FireHoldPriorityOrderSplitter, self).__init__(name, sons, logger)

        self.order_changer = order_changer
        self.publish_scheduler = publish_scheduler

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

        orders = self.split_fire_hold_priority(order)
        self._schedule_hold_orders(orders)
        self._remove_split_tags(orders)
        self._add_fire_hold_tag(orders)

        return orders

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

        orders = []
        main_order_items = []
        split_index = 1
        for item in order.items:
            if not must_stay_in_main_order(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 hold_item or fire_item or priority_item:
                property_list = [hold_in_item, fire_in_item, priority_in_item]
                clear_item = self._get_item_without_properties(
                    item=item.clone(),
                    item_has_properties=property_list,
                )
                if clear_item is not None:
                    main_order_items.append(clear_item)

        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,               # type: FireHoldPriorityOrderSplitter
        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(), hold_in_item)
        if not hold_item_list:
            return hold_item_list, split_index

        hold_times_items = {}
        for hold_item in hold_item_list:
            property_list = [fire_in_item, priority_in_item]
            remove_tags_list = [clear_fire, clear_priority]
            hold_item = self._get_item_with_property(
                item=hold_item,
                item_has_property=hold_in_item,
                item_has_not_property=property_list,
                clear_tags=remove_tags_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 and hold_type != "hold_0":
                    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,       # type: FireHoldPriorityOrderSplitter
        orders,     # type: List[ProductionOrder]
    ):
        # type: (...) -> None

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

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

    def _get_min_hold_time(
        self,
        first_store_time,       # type: datetime
        orders,                 # type: List[ProductionOrder]
        datetime_now,           # type: datetime
    ):
        # type: (...) -> datetime

        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:
                min_hold_time = display_time if min_hold_time is None or display_time < min_hold_time else min_hold_time
                continue

            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)

        return min_hold_time

    def _remove_split_tags(
        self,       # type: FireHoldPriorityOrderSplitter
        orders,     # type: List[ProductionOrder]
    ):
        # type: (...) -> None

        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,       # type: FireHoldPriorityOrderSplitter
        orders,     # type: List[ProductionOrder]
    ):
        # type: (...) -> None

        for order in orders:
            main_order = "-0" in order.order_id
            for item in order.items:
                fire_checker = 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 = 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,           # type: FireHoldPriorityOrderSplitter
        item,           # type: Item
        split_id,       # type: str
    ):
        # type: (...) -> None

        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,      # type: ProductionOrder
    ):
        # type: (...) -> Optional[int]

        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:
            return None

    def _remove_hold_from_items(
        self,       # type: FireHoldPriorityOrderSplitter
        item,       # type: Item
    ):
        # type: (...) -> None

        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,     # type: Optional[List[ProductionOrder]]
    ):
        # type: (...) -> Optional[datetime]

        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, "%Y-%m-%dT%H:%M:%S")

        return first_store_time

    @staticmethod
    def _get_hold_time(
        item,       # type: Item
    ):
        # type: (...) -> int

        try:
            hold_time = int(item.properties.get("hold"))
        except Exception:
            hold_time = -1

        return hold_time

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

        property_list = [hold_in_item, priority_in_item]
        remove_tags_list = [clear_hold, clear_priority]
        fire_item = self._get_item_with_property(
            item=item.clone(),
            item_has_property=fire_in_item,
            item_has_not_property=property_list,
            clear_tags=remove_tags_list,
        )
        if fire_item:
            fire_item.set_fire()
            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,               # type: FireHoldPriorityOrderSplitter
        item,               # type: Item
        order,              # type: ProductionOrder
        orders,             # type: List[ProductionOrder]
        split_index,        # type: int
    ):
        # type: (...) -> Tuple[Any, int]

        property_list = [fire_in_item, hold_in_item]
        remove_tags_list = [clear_hold, clear_fire]
        priority_item = self._get_item_with_property(
            item=item.clone(),
            item_has_property=priority_in_item,
            item_has_not_property=property_list,
            clear_tags=remove_tags_list,
        )
        if priority_item:
            priority_item.set_high_priority()
            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,       # type: FireHoldPriorityOrderSplitter
        root,       # type: Any[ProductionOrder, Item]
    ):
        # type: (...) -> 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

    def _get_item_with_property(
        self,                   # type: FireHoldPriorityOrderSplitter
        item,                   # type: Item
        item_has_property,      # type: Callable[[Item], bool]
        item_has_not_property,  # type: List[Callable]
        clear_tags,             # type: List[Callable]
    ):
        # type: (...) -> Optional[Item]

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

            item.items = children
            return item

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

        for clear_tag in clear_tags:
            clear_tag(item=item)

        item.items = children
        return item if children else None

    def _get_item_without_properties(
        self,                       # type: FireHoldPriorityOrderSplitter
        item,                       # type: Item
        item_has_properties,        # type: List[Callable[[Item], bool]]
    ):
        # type: (...) -> Optional[Item]

        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)
        else:
            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

    @staticmethod
    def _hold_in_item_properties(
        item,  # type: Item
    ):
        # type: (...) -> bool

        return "hold" in item.properties

    @staticmethod
    def _fire_in_item_properties(
        item,  # type: Item
    ):
        # type: (...) -> bool

        return "fire" in item.properties
