import json
import time
from typing import (
    Any,
    Dict,
    List,
    Optional,
    Tuple,
    Union,
)
from xml.etree import cElementTree as eTree

from helper import round_half_away_from_zero

from application.model.benefit import (
    ActionResult,
    ActionTypes,
    Benefit,
    OrderBenefitInfo,
)
from application.model.exceptions import DiscountNotFound


def benefit_is_add_item_type_on_total(
    benefit,    # type: Dict
):
    # type: (...) -> bool

    actions = benefit.get("actions", {})
    return any([
        action.get("itemsToAdd", False) and action.get("automatic") == "ON_TOTAL"
        for _, action in actions.items()
    ])


def split_benefit_key(
    benefit_key,    # type: str
):
    # type: (...) -> Tuple[str, str]

    benefit_id, applied_time = benefit_key.split("_")
    return benefit_id, applied_time


def get_benefit_to_apply_from_persisted_benefits(
    persisted_benefits,     # type: Dict
    benefit_key=None,       # type: Optional[str]
    default_benefit=None,   # type: Optional[Dict]
):
    # type: (...) -> Dict

    if not benefit_key or not persisted_benefits:
        return default_benefit

    return persisted_benefits["pending_benefits"].get(benefit_key, default_benefit)


def get_benefit_key_from_persisted_benefits(
    benefit_id,             # type: str
    persisted_benefits,     # type: Dict
):
    # type: (...) -> Optional[bool]

    if not benefit_id or not persisted_benefits:
        return None

    persisted_benefits_ids = persisted_benefits.get("pending_benefits_ids", [])

    benefit_key_tuple = filter(lambda b: b[1] == benefit_id, persisted_benefits_ids)

    return "{}_{}".format(benefit_key_tuple[0][1], benefit_key_tuple[0][0]) if benefit_key_tuple else None


def get_benefit_key(
    benefit_id,     # type: str
):
    # type: (...) -> str

    return "{}_{}".format(benefit_id, int(round(time.time() * 1000)))


def get_new_benefit(
    uow,            # type: Any
    action_result,  # type: Optional[ActionResult]
    old_benefits,   # type: List
    benefit_key,    # type: str
    order_state,    # type: str
):
    # type: (...) -> str

    benefit_total_value = sum_discounts(
        promotion_tender=uow.promotion_tenders,
        discount_item_values=action_result.discount_item_values,
    )
    new_benefits = [dict(
        benefit=uow.raw_benefit,
        benefit_id=uow.raw_benefit.get("name"),
        benefit_total_value=benefit_total_value,
        burnt_items=action_result.items_to_burn,
        added_sale_lines=uow.added_sale_lines,
        promotion_tender=uow.promotion_tenders,
        benefit_applier=uow.benefit_applier,
        used_discount_ids=action_result.discounts_id,
        order_tenders_ids=action_result.order_tenders,
        discount_item_values=action_result.discount_item_values,
        benefit_applied=action_result.benefit_applied,
        order_state=order_state,
        benefit_key=benefit_key,
        times_to_apply=uow.benefit.times_to_apply)
    ]

    return json.dumps(old_benefits + new_benefits)


def get_applied_benefits_by_key(
    benefit_keys,       # type: List[str]
    applied_benefits,   # type: List[Dict]
):
    # type: (...) -> List

    matching_benefits = []
    for applied_benefit in applied_benefits:
        applied_benefit_key = applied_benefit.get("benefit_key")
        if applied_benefit_key in benefit_keys:
            matching_benefits.append(applied_benefit)

    if not matching_benefits:
        raise DiscountNotFound()

    return matching_benefits


def is_order_taker(
    model,  # type: eTree
):
    # type: (...) -> bool

    working_mode = model.find(".//WorkingMode")
    pos_function = working_mode.get("posFunction")

    if pos_function is not None and pos_function == "OT":
        return True
    return False


def get_error_message(
    order_id,   # type: int
    benefit,    # type: Benefit
):
    # type: (...) -> str

    return "Error applying benefit for order {} - Benefit name: {}".format(str(order_id), benefit.name)


def get_dict_data_remove(
    benefits,       # type: Dict
    dict_data,      # type: Dict
    property_name,  # type: str
):
    # type: (...) -> Dict

    ids = benefits.get(property_name, {})
    for data_id in dict_data.keys():
        if data_id not in ids.keys():
            ids[data_id] = dict_data[data_id]

    return ids


def get_list_data_remove(
    benefits,       # type: Dict
    list_data,      # type: List
    property_name,  # type: str
):
    # type: (...) -> List

    if len(benefits) == 0:
        return []

    ids = benefits.get(property_name, "[]")
    for data_id in list_data:
        if data_id not in ids:
            ids.append(data_id)

    return ids


def remove_dict_properties_benefits(
    benefits,       # type: Dict
    dict_remove,    # type: Dict
    property_name,  # type: str
):
    # type: (...) -> Dict

    result = {}
    body = benefits.get(property_name, {})
    for key in body.keys():
        if key not in dict_remove.keys():
            result[key] = body[key]

    return result


def remove_list_properties_benefits(
    benefits,       # type: Dict
    list_remove,    # type: List
    property_name,  # type: str
):
    # type: (...) -> List

    result = []
    keys = benefits.get(property_name, "[]")
    for key in keys:
        if key not in list_remove:
            result.append(key)

    return result


def sum_discount_items(
    discount_item_values,   # type: Dict
):
    # type: (...) -> float

    total_discount = 0
    for key, value in discount_item_values.items():
        if isinstance(value, float):
            total_discount += value
            continue

        total_discount += sum_discount_items(discount_item_values=value)

    return total_discount


def sum_discounts(
    promotion_tender,       # type: Dict
    discount_item_values,   # type: Dict
):
    # type: (Dict, Dict) -> float

    sum_promotion_tender = sum([float(x.get("amount")) for x in promotion_tender])
    sum_discount_item_values = sum_discount_items(discount_item_values=discount_item_values)

    return sum_promotion_tender + sum_discount_item_values


def get_remaining_amount_discount(
    max_value_to_discount,  # type: float
    actual_value=0,         # type: float
):
    # type: (...) -> float

    if max_value_to_discount is None:
        return float("inf")

    return max_value_to_discount - actual_value


def get_line_id_from_sale_line(
    sale_line,          # type: Any
    from_xml=False,     # type: Optional[bool]
):
    # type: (...) -> str

    if from_xml:
        return "{}.{}-{}.{}".format(
            sale_line.get("lineNumber"),
            sale_line.get("level"),
            sale_line.get("itemId"),
            sale_line.get("partCode"),
        )

    return "{}.{}-{}.{}".format(
        sale_line.line_number,
        sale_line.level,
        sale_line.context,
        sale_line.item_id,
    )


def round_value(
    value,      # type: float
    decimal=2,  # type: Optional[int]
):
    # type: (...) -> Optional[Union[float, int]]

    if value is not None:
        return round_half_away_from_zero(
            number=value,
            precision=decimal,
        )

    return None


def is_invalid_sale_line_or_remaining_quantity(
    restriction,            # type: Restrictions
    sale_line,              # type: SaleLine
    remaining_quantity,     # type: Any[Union[int, float]]
):
    # type: (...) -> bool

    return not restriction.validate(sale_line) or (remaining_quantity is not None and remaining_quantity == 0)


def subtract_quantity(
    value,              # type: Any[Union[int, float]]
    value_to_subtract,  # type: Any[Union[int, float]]
):
    # type: (...) -> Any[Union[int, float]]

    return None if value is None else value - value_to_subtract


def add_to_dict_value(
    object_dict,    # type: Dict
    dict_key,       # type: Any
    value,          # type: Any[Union[int, float]]
):
    # type: (...) -> None

    if dict_key in object_dict.keys():
        object_dict[dict_key] += value
    else:
        object_dict[dict_key] = value


def have_action_by_items_or_groups(
    benefit,            # type: Benefit
    on_total=False,     # type: Optional[bool]
):
    # type: (...) -> bool

    if on_total:
        action_types = [
            ActionTypes.discountAmountByItemsOnTotal.value,
            ActionTypes.discountPercentageByItemsOnTotal.value,
            ActionTypes.discountAmountByGroupsOnTotal,
            ActionTypes.discountPercentageByGroupsOnTotal,
        ]
    else:
        action_types = [
            ActionTypes.discountAmountByItems.value,
            ActionTypes.discountPercentageByItems.value,
        ]

    for action in benefit.actions:
        if action.name in action_types:
            return True

    return False


def get_item_values_line_ids(
    benefit,    # type: OrderBenefitInfo
):
    item_values = benefit.discount_item_values.values()
    item_values_line_ids = []
    for item_value in item_values:
        item_values_line_ids.extend(item_value.keys())
    return item_values_line_ids
