import copy
import re
from operator import attrgetter

from typing import (
    Dict,
    List,
    Optional,
)
from xml.etree import ElementTree
from xml.sax.saxutils import escape

from old_helper import round_half_away_from_zero
from pos_model import (
    SaleItemType,
    SimplifiedSaleItem,
    TaxItem,
)


class SaleLineUtil(object):
    def __init__(self):
        return

    def get_product_name_from_tax_item(self, tax_item, sale_lines):
        # type: (TaxItem, List[ElementTree]) -> unicode
        sale_line = self._get_sale_line_by_tax_item(tax_item, sale_lines)

        if sale_line is None:
            raise Exception("SaleLine not found for product: {0}.{1}".format(tax_item.item_id, tax_item.part_code))

        product_name = escape(sale_line.get("productName"))

        return product_name

    def get_sale_line_by_tax_item(self, tax_item, sale_lines):
        # type: (TaxItem, List[ElementTree]) -> Optional[ElementTree]
        for sale_line in sale_lines:
            if sale_line.get("itemId") == tax_item.item_id and int(sale_line.get("partCode")) == tax_item.part_code:
                return sale_line

        raise Exception("SaleLine not found for product: {0}.{1}".format(tax_item.item_id, tax_item.part_code))


def simplify_sale_items(
    sale_items,                 # type: List[SimplifiedSaleItem]
    ignore_context=False,       # type: bool
    hide_unpriced_items=False,  # type: bool
):
    # type: (...) -> List[SimplifiedSaleItem]

    _validate_flat_list(sale_items)

    if ignore_context:
        result_sale_items = _simplify_sale_items_by_part_code(
            sale_items,
            hide_unpriced_items=hide_unpriced_items,
        )
    else:
        result_sale_items = _simplify_sale_items_by_context(
            sale_items,
            hide_unpriced_items=hide_unpriced_items,
        )

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


def _validate_flat_list(
    sale_items,  # type: List[SimplifiedSaleItem]
):
    # type: (...) -> 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")


def _simplify_sale_items_by_part_code(
    sale_items,                  # type: List[SimplifiedSaleItem]
    hide_unpriced_items=False,   # type: bool
):
    # type: (...) -> List[SimplifiedSaleItem]

    related_keys = {}  # type: Dict[str, List[SimplifiedSaleItem]]
    base_key = "{};item_price={!r}"

    for index, item in enumerate(copy.deepcopy(sale_items)):
        hide_item = item.sale_item.item_price == 0 and hide_unpriced_items
        if hide_item or item.sale_item.quantity == 0:
            continue

        item.key = base_key.format(
            item.sale_item.part_code,
            _get_actual_unit_price(item),
        )
        item.original_position = index
        related_keys.setdefault(item.key, []).append(item)

    result_sale_items = []  # type: List[SimplifiedSaleItem]
    for _, group in related_keys.items():
        result_sale_items.extend(_merge_sale_item_list(group))

    return result_sale_items


def _simplify_sale_items_by_context(
    sale_items,                 # type: List[SimplifiedSaleItem]
    hide_unpriced_items=False,  # type: bool
):
    # type: (...) -> List[SimplifiedSaleItem]

    refined_sale_items = _refine_sale_items(
        copy.deepcopy(sale_items),
        hide_unpriced_items=hide_unpriced_items,
    )

    if not hide_unpriced_items:
        return _merge_full_groups(refined_sale_items)

    return _merge_sale_item_list(refined_sale_items)


def _refine_sale_items(
    sale_items,                 # type: List[SimplifiedSaleItem]
    hide_unpriced_items=False,  # type: bool
):
    # type: (...) -> List[SimplifiedSaleItem]

    removal_list = []

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

    return _remove_items(
        removal_list,
        sale_items,
    )


def _update_item_properties(
    index,              # type: int
    item,               # type: SimplifiedSaleItem
    last_item_index,    # type: int
    sale_items,         # type: List[SimplifiedSaleItem]
):
    # type: (...) -> None

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

    if item.grouping_context is None:
        item.grouping_context = "{}.{}".format(
            item.sale_item.item_id,
            item.sale_item.part_code,
        )


def _remove_items(
    removal_list,   # type: List[SimplifiedSaleItem]
    sale_items,     # type: List[SimplifiedSaleItem]
):
    # type: (...) -> List[SimplifiedSaleItem]

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

    for item in sale_items:
        if item in removal_list:
            continue

        item.key = base_key.format(
            item.grouping_context,
            _get_actual_unit_price(item),
            item.sale_item.level,
        )
        result_sale_items.append(item)

    return result_sale_items


def _refine_sale_items_sons(
    parent_item,   # type: SimplifiedSaleItem
    items_subset,  # type: List[SimplifiedSaleItem]
):
    # type: (...) -> None

    part_code_pattern = re.compile(r"\.{}\.?".format(parent_item.sale_item.part_code))
    parent_context = "{}.{}".format(
        parent_item.sale_item.item_id,
        parent_item.sale_item.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 "{}.{}".format(
                item.sale_item.item_id,
                item.sale_item.part_code,
            )
            item.grouping_context = re.sub(
                part_code_pattern,
                ".",
                item_context,
                count=1,
            ).rstrip(".")
        else:
            break


def _merge_full_groups(
    refined_sale_items,  # type: List[SimplifiedSaleItem],
):
    # type: (...) -> List[SimplifiedSaleItem]

    grouped_items = {}  # type: Dict[str, List[SimplifiedSaleItem]]
    related_items = []  # type: List[SimplifiedSaleItem]
    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 = []  # type: List[SimplifiedSaleItem]
    for _, items in grouped_items.items():
        result_sale_items.extend(_merge_sale_item_list(items))

    return result_sale_items


def _merge_sale_item_list(
    items,  # type: List[SimplifiedSaleItem]
):
    # type: (...) -> List[SimplifiedSaleItem]

    result_merge = []  # type: List[SimplifiedSaleItem]
    merged_items = []  # type: List[SimplifiedSaleItem]

    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 = _merge_sale_items(item_ref, item)
                merged_items.append(item)

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

    return result_merge


def _merge_sale_items(
    sl_a,  # type: SimplifiedSaleItem
    sl_b,  # type: SimplifiedSaleItem
):
    # type: (...) -> SimplifiedSaleItem

    sale_item = copy.deepcopy(sl_a.sale_item)
    sale_item.item_price = sl_a.sale_item.item_price + sl_b.sale_item.item_price
    sale_item.quantity = sl_a.sale_item.quantity + sl_b.sale_item.quantity
    sale_item.default_quantity = (sl_a.sale_item.default_quantity or 0) + (sl_b.sale_item.default_quantity or 0)
    multiplied_quantity = (sl_a.sale_item.multiplied_quantity or 0) + (sl_b.sale_item.multiplied_quantity or 0)
    sale_item.multiplied_quantity = multiplied_quantity
    original_item_price = sl_a.original_item_price or sl_a.sale_item.item_price
    return SimplifiedSaleItem(
        sale_item,
        order_id=None,
        key=sl_a.key,
        is_cfh_head=sl_a.is_cfh_head,
        grouping_context=sl_a.grouping_context,
        original_position=min(
            sl_a.original_position,
            sl_b.original_position,
        ),
        original_reference=sl_a.original_reference,
        original_item_price=original_item_price,
    )


def _get_actual_unit_price(
    item,  # type: SimplifiedSaleItem
):
    # type: (...) -> float

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

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


def _is_combo(
    item,  # type: SimplifiedSaleItem
):
    # type: (...) -> bool

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


def _is_inner_cfh(
    item,  # type: SimplifiedSaleItem
):
    # type: (...) -> bool

    if not _is_combo(item):
        return False

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


def _is_option(
    item,  # type: SimplifiedSaleItem
):
    # type: (...) -> bool

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