from logging import Logger
from datetime import datetime, timedelta

from production.box import ProductionBox
from production.model import Item, ProductionOrder
from typing import List, Dict, Union, Any, Optional

from ._OrderChangerProductionBox import OrderChangerProductionBox


class CourseBox(OrderChangerProductionBox):
    def __init__(self, name, sons, courses, publish_scheduler, logger=None):
        # type: (str, Union[ProductionBox, List[ProductionBox]], List[CourseConfiguration], Any, Logger) -> None  # noqa

        super(CourseBox, self).__init__(name, sons, logger)
        self.courses = courses
        self.publish_scheduler = publish_scheduler
        self.course_dict = {}
        self.course_name_dict = {}
        for course_index in range(0, len(self.courses)):
            for part_code in courses[course_index].products:
                self.course_dict[part_code] = course_index
                self.course_name_dict[courses[course_index].name] = course_index

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

        if len(self.courses) == 0 or order.skip_course:
            return order

        courses = self.separate_product_in_courses(order)
        valid_courses_count = self.count_valid_courses(courses)
        if valid_courses_count <= 1:
            return order

        orders = []
        split_index = 0
        all_previous_is_ready = True
        current_course = self.get_first_not_ready_course(courses)

        for course in courses:
            if len(course.products) == 0:
                continue

            if course == current_course:
                current_order = self._get_current_course_order(courses, current_course, order)
                new_order = current_order.clone(index=split_index)
            else:
                product = course.products if all_previous_is_ready else []
                new_order = order.clone(product, split_index)
            orders.append(new_order)
            split_index += 1

            all_previous_is_ready = course.ready and all_previous_is_ready

        return orders

    def _get_current_course_order(self, courses, current_course, order):
        # type: (List[Course], Course, ProductionOrder) -> ProductionOrder

        now = datetime.now()
        last_done_time = self.get_last_done_time_of_course(courses[current_course.index - 1])
        course_delay_ok = self.has_elapsed_course_interval(now, courses[current_course.index - 1], last_done_time)
        if last_done_time is None or course_delay_ok:
            order.items = current_course.products

            if last_done_time is not None:
                display_time = self.get_display_time(courses[current_course.index - 1], last_done_time)
                order.display_time = display_time.isoformat()
        else:
            order.items = []

            wait_time = self.calculate_wait_time(now, courses[current_course.index - 1], last_done_time)
            if wait_time > 0:
                self.publish_scheduler.schedule_publish(order.order_id, wait_time)

        return order

    @staticmethod
    def get_first_not_ready_course(courses):
        # type: (List[Course]) -> Optional[Course]

        for course in courses:
            if not course.ready:
                return course
        return None

    @staticmethod
    def count_valid_courses(courses):
        # type: (List[Course]) -> int

        valid_courses = [x for x in courses if len(x.products) > 0]
        return len(valid_courses)

    def separate_product_in_courses(self, order):
        # type: (ProductionOrder) -> List

        course_products = {k: [] for k in range(-1, len(self.courses))}
        course_ready = {k: True for k in range(-1, len(self.courses))}
        course_done = {k: True for k in range(-1, len(self.courses))}

        self._populate_course_products(course_products, order.items)
        self._handle_products_without_course(course_products, True)
        for course_index in range(0, len(self.courses)):
            for item in course_products[course_index]:
                if item.multiplied_qty > 0 and not item.is_prepared():
                    course_ready[course_index] = False

                if item.multiplied_qty > 0 and not item.is_done():
                    course_done[course_index] = False

        self._send_fire_items_to_first_available_course(course_products)

        return self._create_courses(course_products, course_ready, course_done)

    @staticmethod
    def _create_courses(course_products, course_ready, course_done):
        # type: (Dict[int, List[Item]], Dict, Dict) -> List[Course]

        courses = []
        for index in range(0, len(course_products)):
            products = course_products[index]
            course = CourseBox.Course(products, course_ready[index], course_done[index], index)
            courses.append(course)

        for index in range(0, len(courses)):
            all_course_items_canceled = True
            for product in courses[index].products:
                if not product.ignore_removed_item():
                    all_course_items_canceled = False
                    break

            if all_course_items_canceled and index < len(courses) - 1:
                for product in courses[index].products:
                    courses[index + 1].products.append(product)

        return courses

    def _populate_course_products(self, course_products, items):
        # type: (Dict[int, List[Item]], List[Item]) -> None

        for item in items:
            self._populate_item_course(item, course_products)

        self._handle_products_without_course(course_products)

    def _send_fire_items_to_first_available_course(self, course_products):
        # type: (Dict[int, List[Item]]) -> None

        course_qty, first_course_with_products = self._count_courses(course_products)
        self._move_fire_products(course_products, course_qty, first_course_with_products)

    @staticmethod
    def _move_fire_products(course_products, course_qty, first_course_with_products):
        # type: (Dict[int, List[Item]], int, int) -> None

        if course_qty > 1:
            for index in range(first_course_with_products + 1, len(course_products)):
                for item in course_products[index][:]:
                    if "fire" in item.properties or "fire" in item.tags:
                        course_products[first_course_with_products].append(item)
                        course_products[index].remove(item)

    @staticmethod
    def _count_courses(course_products):
        # type: (Dict[int, List[Item]]) -> (int, int)

        course_qty = 0
        first_course_with_products = None
        for course_index in course_products:
            if len(course_products[course_index]) > 0:
                course_qty += 1
                if first_course_with_products is None:
                    first_course_with_products = course_index
        return course_qty, first_course_with_products

    def _populate_item_course(self, item, course_products):
        # type: (Item, Dict[int, List[Item]]) -> None

        if self.item_has_course_set(item):
            item_course_index = self.get_course_index_from_set_course(item)
            course_products[item_course_index].append(item)
            return

        if item.part_code in self.course_dict:
            item_course_index = self.course_dict[item.part_code]
            course_products[item_course_index].append(item)
            return

        if item.item_type not in ("COMBO", "OPTION"):
            course_products[-1].append(item)
            return

        son_course_products = {k: [] for k in range(-1, len(self.courses))}
        self._populate_course_products(son_course_products, item.items)

        for key, value in son_course_products.items():
            if len(value) == 0:
                continue

            new_combo = item.clone()
            new_combo.items = value
            course_products[key].append(new_combo)

    def _handle_products_without_course(self, course_products, process_products_without_course=False):
        has_product_without_course = -1 in course_products

        if has_product_without_course:
            for course_index in range(0, len(self.courses)):
                if len(course_products[course_index]) > 0:
                    course_products[course_index].extend(course_products[-1])
                    del course_products[-1]
                    break
            else:
                if process_products_without_course:
                    course_products[0].extend(course_products[-1])
                    del course_products[-1]

    def item_has_course_set(self, item):
        # type: (Item) -> bool

        if item.properties is not None:
            return "course" in item.properties and item.properties["course"] in self.course_name_dict

        return False

    def get_course_index_from_set_course(self, item):
        # type: (Item) -> int

        return self.course_name_dict[item.properties["course"]]

    def has_elapsed_course_interval(self, now, course, last_done_time):
        # type: (datetime, Course, datetime) -> bool
        if last_done_time is None:
            return True

        next_course_ready = last_done_time + timedelta(seconds=self.courses[course.index].next_course_wait_time)
        return int((now - next_course_ready).total_seconds()) >= 0

    def get_display_time(self, course, last_done_time):
        # type: (Course, datetime) -> datetime

        course_wait = self.courses[course.index].next_course_wait_time
        return last_done_time + timedelta(seconds=course_wait)

    def calculate_wait_time(self, now, course, last_done_time):
        # type: (datetime, Course, datetime) -> int

        wait_time = ((last_done_time + timedelta(seconds=self.courses[course.index].next_course_wait_time)) - now)
        return int(wait_time.total_seconds())

    @staticmethod
    def get_last_done_time_of_course(course):
        # type: (Course) -> Optional[datetime]

        last_done_time = None
        for item in course.products:
            done_time = item.get_last_prepared_time()
            if done_time is not None and (last_done_time is None or last_done_time < done_time):
                last_done_time = done_time

        return last_done_time

    class CourseConfiguration(object):
        def __init__(self, name, products, next_course_wait_time):
            self.name = name
            self.products = products
            self.next_course_wait_time = next_course_wait_time

    class Course(object):
        def __init__(self, products, ready, done, index):
            self.products = products
            self.ready = ready
            self.done = done
            self.index = index
