import copy
import re

from operator import attrgetter

from helper import round_half_away_from_zero
from typing import (
    List,
    Optional,
    Dict,
)

from order_api.dto import SaleLine as SaleLineApi


class GroupedSaleItems(object):
    def __init__(
        self,
        sale_item: Optional[SaleLineApi] = None,
        order_id: Optional[int] = None,
        key: Optional[str] = None,
        is_cfh_head: Optional[bool] = None,
        grouping_context: Optional[str] = None,
        original_position: Optional[int] = None,
        original_reference: Optional[SaleLineApi] = None,
        original_item_price: Optional[float] = None,
        has_fraction: Optional[bool] = None,
        original_related_line_ids: Optional[List[str]] = None,
        service_period: Optional[str] = None,
    ) -> None:

        self.sale_item = sale_item
        self.order_id = order_id
        self.key = key
        self.is_cfh_head = is_cfh_head
        self.grouping_context = grouping_context
        self.original_position = original_position
        self.original_reference = original_reference
        self.original_item_price = original_item_price
        self.has_fraction = has_fraction
        self.original_related_line_ids = original_related_line_ids
        self.service_period = service_period

        if self.sale_item and self.original_related_line_ids is None:
            formatted_sale_line = self.sale_item.get_line_id()
            self.original_related_line_ids = [formatted_sale_line]

    def grouped_sale_lines(
        self,
        sale_lines: List[SaleLineApi],
        hide_items_without_price: bool = False,
        fraction_line_numbers: Optional[List[str]] = None,
    ) -> List[SaleLineApi]:

        items = [
            self._create_simplified_sale_item(
                sale_line=sale_line,
                fraction_line_numbers=fraction_line_numbers,
            ) for sale_line in sale_lines
        ]
        simplified_sale_items = self._grouped_items(
            sale_items=items,
            hide_items_without_price=hide_items_without_price,
        )
        result_sale_lines = []

        for item in simplified_sale_items:
            sale_line = item.original_reference
            sale_line.level = item.sale_item.level
            sale_line.item_price = item.sale_item.item_price
            sale_line.quantity = item.sale_item.quantity
            sale_line.original_related_line_ids = item.original_related_line_ids
            if item.sale_item.default_qty is not None:
                sale_line.default_qty = item.sale_item.default_qty

            result_sale_lines.append(sale_line)

        return result_sale_lines

    def _grouped_items(
        self,
        sale_items: List["GroupedSaleItems"],
        hide_items_without_price: Optional[bool] = False,
    ) -> List["GroupedSaleItems"]:

        self._validate_flat_list(sale_items=sale_items)

        result_sale_items = self._simplify_sale_items_by_context(
            sale_items=sale_items,
            hide_items_without_price=hide_items_without_price,
        )

        result_sale_items.sort(key=attrgetter("original_position"))
        return result_sale_items

    @staticmethod
    def _validate_flat_list(
        sale_items: List["GroupedSaleItems"]
    ) -> None:

        for item in sale_items:
            if item.sale_item.parent is not None or item.sale_item.sons:
                raise ValueError("Simplification requires flat SaleItem references")

    @staticmethod
    def _create_simplified_sale_item(
        sale_line: SaleLineApi,
        fraction_line_numbers: Optional[Dict] = None,
    ) -> "GroupedSaleItems":

        order_id = sale_line.get_custom_property("orderId")
        sale_line_identifier = f"{order_id}-{sale_line.line_number}"
        item = GroupedSaleItems(
            sale_item=sale_line,
            order_id=order_id,
            original_reference=sale_line,
            has_fraction=sale_line_identifier in fraction_line_numbers,
            service_period=sale_line.service_period,
        )
        return item

    def _simplify_sale_items_by_context(
        self,
        sale_items: List["GroupedSaleItems"],
        hide_items_without_price: bool,
    ) -> List["GroupedSaleItems"]:

        refined_sale_items = self._refine_sale_items(
            sale_items=copy.deepcopy(sale_items),
            hide_items_without_price=hide_items_without_price,
        )

        if not hide_items_without_price:
            return self._merge_full_groups(refined_sale_items=refined_sale_items)

        return self._merge_sale_item_list(items=refined_sale_items)

    @staticmethod
    def _get_actual_unit_price(
        item: "GroupedSaleItems",
    ) -> float:

        item_price = item.original_item_price or item.sale_item.item_price
        quantity = item.sale_item.quantity
        if item.sale_item.multiplied_qty is not None:
            quantity *= item.sale_item.multiplied_qty

        item_price /= quantity
        return round_half_away_from_zero(
            number=item_price,
            precision=2,
        )

    def _merge_full_groups(
        self,
        refined_sale_items: List["GroupedSaleItems"],
    ) -> List["GroupedSaleItems"]:

        grouped_items: Dict[str, List[GroupedSaleItems]] = {}
        related_items: List[GroupedSaleItems] = []
        current_order_id = None
        current_line_number = None

        for index, item in enumerate(refined_sale_items):
            same_order_id = current_order_id == item.order_id
            same_line_number = current_line_number == item.sale_item.line_number
            if not same_order_id or not same_line_number:
                if related_items:
                    k = "|".join(sorted(i.key for i in related_items))
                    grouped_items.setdefault(k, []).extend(related_items)

                related_items = []
                current_order_id = item.order_id
                current_line_number = item.sale_item.line_number

            related_items.append(item)

        if related_items:
            k = "|".join(sorted(i.key for i in related_items))
            grouped_items.setdefault(k, []).extend(related_items)

        result_sale_items: List[GroupedSaleItems] = []
        for _, items in grouped_items.items():
            result_sale_items.extend(self._merge_sale_item_list(items=items))

        return result_sale_items

    def _merge_sale_item_list(
        self,
        items: List["GroupedSaleItems"],
    ) -> List["GroupedSaleItems"]:

        result_merge: List[GroupedSaleItems] = []
        merged_items: List[GroupedSaleItems] = []
        for index, item_ref in enumerate(items):
            for item in items[index + 1:]:
                if item_ref.key == item.key and item not in merged_items and not item.has_fraction:
                    item_ref = self._merge_sale_items(
                        item_ref=item_ref,
                        item=item,
                    )
                    merged_items.append(item)

            if item_ref not in merged_items:
                result_merge.append(item_ref)

        return result_merge

    @staticmethod
    def _merge_sale_items(
        item_ref: "GroupedSaleItems",
        item: "GroupedSaleItems",
    ) -> "GroupedSaleItems":

        sale_item = copy.deepcopy(item_ref.sale_item)
        sale_item.item_price = item_ref.sale_item.item_price + item.sale_item.item_price
        sale_item.quantity = item_ref.sale_item.quantity + item.sale_item.quantity
        sale_item.default_quantity = (item_ref.sale_item.default_qty or 0) + (item.sale_item.default_qty or 0)
        multiplied_quantity = (item_ref.sale_item.multiplied_qty or 0) + (item.sale_item.multiplied_qty or 0)
        sale_item.multiplied_qty = multiplied_quantity
        original_item_price = item_ref.original_item_price or item_ref.sale_item.item_price
        return GroupedSaleItems(
            sale_item=sale_item,
            key=item_ref.key,
            is_cfh_head=item_ref.is_cfh_head,
            grouping_context=item_ref.grouping_context,
            original_position=min(
                item_ref.original_position,
                item.original_position,
            ),
            original_reference=item_ref.original_reference,
            original_item_price=original_item_price,
            original_related_line_ids=item_ref.original_related_line_ids + item.original_related_line_ids,
            service_period=item_ref.service_period,
        )

    def _refine_sale_items(
        self,
        sale_items: List["GroupedSaleItems"],
        hide_items_without_price: bool,
    ) -> List["GroupedSaleItems"]:

        removal_list = []
        last_item_index = len(sale_items) - 1
        for index, item in enumerate(sale_items):
            item.original_position = index
            is_inner_cfh_item = self._is_inner_cfh(item=item)
            has_quantity = item.sale_item.quantity > 0
            hide_item = item.sale_item.item_price == 0 and hide_items_without_price
            if is_inner_cfh_item or not has_quantity or self._is_option(item=item) or hide_item:
                self._refine_sale_items_sons(
                    parent_item=item,
                    items_subset=sale_items[index + 1:],
                )
                removal_list.append(item)
                continue

            self._update_item_properties(
                index=index,
                item=item,
                last_item_index=last_item_index,
                sale_items=sale_items,
            )

        return self._remove_items(
            removal_list=removal_list,
            sale_items=sale_items,
        )

    @staticmethod
    def _refine_sale_items_sons(
        parent_item: "GroupedSaleItems",
        items_subset: List["GroupedSaleItems"],
    ) -> None:

        part_code_pattern = re.compile(r"\.{}\.?".format(parent_item.sale_item.part.part_code))
        parent_context = f"{parent_item.sale_item.item_id}.{ parent_item.sale_item.part.part_code}"
        for item in items_subset:
            same_order_id = item.order_id == parent_item.order_id
            same_line_number = item.sale_item.line_number == parent_item.sale_item.line_number
            starts_with_parent_item_id = item.sale_item.item_id.startswith(parent_context)
            item_is_son = starts_with_parent_item_id and same_order_id and same_line_number
            if item_is_son:
                item.sale_item.level -= 1
                item_context = item.grouping_context or f"{item.sale_item.item_id}.{ item.sale_item.part.part_code}"
                item.grouping_context = re.sub(
                    pattern=part_code_pattern,
                    repl=".",
                    string=item_context,
                    count=1,
                ).rstrip(".")
            else:
                break

    def _update_item_properties(
        self,
        index: int,
        item: "GroupedSaleItems",
        last_item_index: int,
        sale_items: List["GroupedSaleItems"],
    ) -> None:

        if index < last_item_index:
            next_item = sale_items[index + 1]
            if self._is_combo(item=item) and self._is_inner_cfh(item=next_item):
                item.is_cfh_head = True

        if item.grouping_context is None:
            item.grouping_context = f"{item.sale_item.item_id}.{item.sale_item.part.part_code}"

    def _remove_items(
        self,
        removal_list: List["GroupedSaleItems"],
        sale_items: List["GroupedSaleItems"],
    ) -> List["GroupedSaleItems"]:

        result_sale_items = []
        base_key = "{};item_price={!r};level={!r};service_period={}"

        for item in sale_items:
            if item in removal_list:
                continue

            item_price = self._get_actual_unit_price(item=item)
            item.key = base_key.format(
                item.grouping_context,
                item_price,
                item.sale_item.level,
                item.service_period or "",
            )
            result_sale_items.append(item)

        return result_sale_items

    def _is_inner_cfh(
        self,
        item: "GroupedSaleItems",
    ) -> bool:

        if not self._is_combo(item=item):
            return False

        matcher = re.match(
            r"^CFH\d+",
            item.sale_item.part.product_name.strip(),
            re.IGNORECASE
        )
        return matcher is not None

    @staticmethod
    def _is_combo(
        item: "GroupedSaleItems",
    ) -> bool:

        item_type = item.sale_item.part.item_type or ""
        return item_type.lower() == "combo"

    @staticmethod
    def _is_option(
        item: "GroupedSaleItems",
    ) -> bool:

        item_type = item.sale_item.part.item_type or ""
        return item_type.lower() == "option"
