import json
import logging

import sysactions

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

from mw_helper import show_filtered_list_box_dialog, show_numpad_dialog, show_message_dialog
from sysactions import (
    show_messagebox,
    get_current_order,
    StopAction,
    get_model,
    show_keyboard,
    show_any_dialog,
)
from helper import (
    remove_accents,
)

from actions.models import PaymentType
from actions.coupon.apply_coupon import apply_coupon
from actions.discount.get_discount import get_visible_discounts
from actions.products import getProducts
from actions.util import (
    get_authorization,
    get_user_level,
)
from application.domain import SaleLineIdentifier
from manager.reports import justification_report
from model.customexception import DiscountMinLevelException, ErrorApplyingDiscount

logger = logging.getLogger("PosActions")


def apply_discount(
    pos_id,                     # type: str
    selected_coupon=None,       # type: Optional[Dict]
    amount=0.0,                 # type: Optional[float]
    user_auth_level=-1,         # type: Optional[int]
    user_auth_name=None,        # type: Optional[str]
    user_auth_id=None,          # type: Optional[str]
    payment_info=None,          # type: Optional[Dict[str, Any]]
):
    # type: (...) -> bool

    try:
        coupons = get_visible_discounts(pos_id)
        selected_coupon = _select_coupon(pos_id, selected_coupon, coupons)
        if not selected_coupon:
            return False

        coupon = selected_coupon.get("coupon", {})
        model = get_model(pos_id)

        try:
            if coupon.get("requestJustification", False):
                if user_auth_id is None:
                    operator = model.find("Operator")
                    user_auth_id = operator.get("id")

                _add_justification_on_coupon(
                    coupon=coupon,
                    pos_id=pos_id,
                    user_auth_id=user_auth_id,
                )

            selected_sale_lines = _get_selected_sale_lines(
                coupon=coupon,
                model=model,
                pos_id=pos_id,
            )

            if coupon.get("askValue", False) and not amount:
                coupon["index"] = _get_discount_type(coupon)
                amount = amount or _get_discount_amount(coupon, pos_id)
                coupon = set_coupon_amount(
                    coupon=coupon,
                    amount=amount,
                )

            benefit_applied = _add_benefit(
                pos_id=pos_id,
                coupon=coupon,
                user_auth_level=user_auth_level,
                sale_lines=selected_sale_lines,
                payment_info=payment_info,
            )

            if benefit_applied:
                _check_and_print_justification(
                    pos_id=pos_id,
                    user_auth=user_auth_name,
                    coupon=coupon,
                )

            return benefit_applied
        except DiscountMinLevelException as ex:
            show_messagebox(pos_id, "$NDISCOUNT_ERROR_USER_MIN_LEVEL")
            user_was_authorized, user_auth = get_authorization(
                pos_id=pos_id,
                min_level=ex.min_level_required,
                model=model
            )
            if user_was_authorized:
                user_xml_str = sysactions.get_user_information(user_auth)
                user_element = eTree.XML(user_xml_str).find("user")
                user_auth_level = int(user_element.attrib["Level"])
                user_auth_id = str(user_element.attrib["UserId"])
                coupon['authorizedBy'] = user_auth_id
                selected_coupon["coupon"] = coupon

                apply_discount(
                    pos_id=pos_id,
                    selected_coupon=selected_coupon,
                    amount=amount,
                    user_auth_level=user_auth_level,
                    user_auth_name=user_element.attrib["UserName"],
                    user_auth_id=user_auth_id,
                )
                return True

    except ErrorApplyingDiscount as ex:
        logger.warn("Error applying discount ${}".format(ex.error_code))
        show_message_dialog(pos_id, "$ERROR", "${}".format(ex.error_code))
    except Exception as ex:
        logger.exception("Error applying discount")
        message = "FATAL_ERROR"
        if hasattr(ex, 'message'):
            message = "${}".format(message)
        show_message_dialog(pos_id, "$ERROR", message)

    return False


def _select_coupon(
    pos_id,             # type: str
    selected_coupon,    # type: Optional[Dict]
    coupons,            # type: List
):
    # type: (...) -> Optional[Dict]

    if selected_coupon:
        return selected_coupon

    if len(coupons) == 0:
        show_messagebox(pos_id, "$DISCOUNT_NOT_FOUND", icon="warning")
        return None

    selected_index = _select_discount(pos_id, coupons)
    if selected_index is None:
        return None

    selected_coupon = filter(lambda c: c["index"] == selected_index + 1, coupons)
    if not selected_coupon:
        return None

    return selected_coupon[0]


def _get_discount_amount(coupon, pos_id):
    model = get_model(pos_id)
    order = get_current_order(model)
    amount, percentage = _ask_discount_amount(pos_id, order, coupon=coupon)
    if percentage is not None:
        amount = percentage

    return amount


def apply_amount_discount(pos_id, pos_ot, order, coupon=None, discount_type=None, discount_value=None):
    amount, _ = _ask_discount_amount(pos_id, order, coupon, discount_type, discount_value)
    pos_ot.applyDiscount(1, amount, forcebyitem=1)


def _ask_discount_amount(pos_id, order, coupon=None, discount_type=None, discount_value=None):
    if not discount_type:
        discount_type = coupon['index']
        title = "$DISCOUNT_VALUE_TITLE" if discount_type == PaymentType.AMOUNT.value else "$DISCOUNT_PERCENTAGE_TITLE"
        mask = "CURRENCY" if discount_type == PaymentType.AMOUNT.value else "NUMBER"
        if not discount_value:
            discount_value = show_numpad_dialog(pos_id, title, "", mask)
            if discount_value in [None, '']:
                raise StopAction()

    if discount_type == PaymentType.AMOUNT.value:
        return discount_value, None

    amount = float(order.get("totalGross"))

    return (amount * discount_value) / 100, discount_value


def _select_discount(pos_id, coupons):
    options = ["{0} - {1}".format(coupon["index"], coupon["name"].encode("UTF-8")) for coupon in coupons]
    return show_filtered_list_box_dialog(pos_id, options, "$SELECT_ONE_COUPON", mask="NOFILTER")


def _add_benefit(
    pos_id,             # type: str
    coupon,             # type: Dict[str, Any]
    user_auth_level,    # type: int
    sale_lines=None,    # type: Optional[List[SaleLineIdentifier]]
    payment_info=None,  # type: Optional[Dict[str, Any]]
):
    # type: (...) -> bool

    authorization_level = int(coupon.get("conditions", {}).get("authorizationLevel", 0))
    if _get_discount_authorization(
        pos_id=pos_id,
        coupon=coupon,
        authorization_level=authorization_level,
    ) is False:
        raise StopAction()

    ret = apply_coupon(
        pos_id=pos_id,
        table_id=None,
        user_auth_level=user_auth_level,
        benefit=coupon,
        sale_lines=sale_lines,
        payment_info=payment_info,
    )

    return ret


def _get_object_json(json_object, key, value):
    return [obj for obj in json_object if obj[key] == value][0]


def set_coupon_amount(coupon, amount):
    if coupon.get("actions", False) and coupon.get("actions").get("0", False):
        action = coupon.get("actions").get("0")
        value_actions = ["discountAmountOnTotal", "discountPercentageOnTotal"]
        group_actions = ["discountPercentageByGroupsOnTotal", "discountAmountByGroupsOnTotal"]
        if any(x for x in value_actions if x in action.keys()):
            action_name = action.keys()[0]
            action[action_name] = amount
            return coupon
        elif any(x for x in group_actions if x in action.keys()):
            action.values()[0][0]["amount"] = amount
            return coupon

    raise StopAction()


def _get_discount_type(coupon):
    if coupon.get("actions", False) and coupon.get("actions").get("0", False):
        action = coupon.get("actions").get("0")
        amount_actions = ["discountAmountOnTotal", "discountAmountByGroupsOnTotal"]
        percentage_actions = ["discountPercentageOnTotal", "discountPercentageByGroupsOnTotal"]
        if any(x for x in amount_actions if x in action.keys()):
            return PaymentType.AMOUNT.value
        elif any(x for x in percentage_actions if x in action.keys()):
            return PaymentType.PERCENTAGE.value

    raise StopAction()


def _get_justification_discount(
    pos_id,     # type: str
):
    # type: (...) -> str

    while True:
        justification = show_keyboard(
            posid=pos_id,
            message="$JUSTIFICATION_DISCOUNT",
            title='',
            timeout=1800000,
            buttons="$OK",
        )
        if justification is None:
            raise StopAction()

        justification = justification.strip()
        if len(justification) < 5:
            show_messagebox(
                posid=pos_id,
                message="$MINIMUM_5_CHARACTERS_REQUIRED",
            )
            continue

        break

    return justification


def _get_discount_authorization(
    pos_id,                 # type: str
    coupon,                 # type: dict
    authorization_level,    # type: int
):
    # type: (...) -> Optional[dict, bool]

    model = get_model(posid=pos_id)
    operator = model.find("./Operator")
    user_id = operator.get("id")
    current_user_level = get_user_level(user_id=user_id)

    if current_user_level >= authorization_level:
        coupon['authorized_by'] = user_id
        return coupon

    user_was_authorized, user_auth = get_authorization(
        pos_id=pos_id,
        min_level=authorization_level,
        model=model,
        access_denied_message="$NDISCOUNT_ERROR_USER_MIN_LEVEL",
    )
    authorized_user_level = get_user_level(user_id=user_auth)
    if user_was_authorized and authorized_user_level >= authorization_level:
        coupon['authorized_by'] = user_auth
        return coupon

    return False


def _check_and_print_justification(
    pos_id,         # type: str
    user_auth,      # type: str
    coupon,         # type: Dict
):

    justification = coupon.get("justification")
    should_print_justification = coupon.get("printJustifyCoupon", False)
    if not justification or not should_print_justification:
        return

    justification_report(
        pos_id=pos_id,
        justification=justification,
        discount_description=coupon.get('userDescription', ""),
        authorized_by=user_auth,
    )


def _add_justification_on_coupon(
    coupon,         # type: Dict[str, Any]
    pos_id,         # type: str
    user_auth_id,   # type: str
):
    # type: (...) -> None

    justification = coupon.get("justification")
    if justification is None:
        justification = _get_justification_discount(
            pos_id=pos_id,
        )
        coupon['justification'] = justification

    coupon['appliedBy'] = user_auth_id


def _get_selected_sale_lines(
    coupon,  # type: Dict[str, Any]
    model,   # type: eTree.Element
    pos_id,  # type: str
):
    # type: (...) -> List[SaleLineIdentifier]

    if not coupon.get("conditions", {}).get("askItemToApplyDiscount"):
        return []

    order = get_current_order(model=model)
    options = _get_options_to_apply_discount(
        pos_id=pos_id,
        sale_lines=order.findall("SaleLine"),
        discount_details=_get_discount_details(order),
        coupon=coupon,
    )

    if not options:
        show_messagebox(pos_id, "$NDISCOUNT_ERROR_REQUIRED_ITEMS_ON_SALE")
        raise StopAction()

    selected_items = _show_multiple_select_list_box_dialog(
        pos_id=pos_id,
        options=map(json.dumps, options),
        title="$SELECT_ONE_OR_MORE_ITEMS",
    )

    if not selected_items:
        raise StopAction()

    selected_sale_lines = [
        SaleLineIdentifier(
            line_number=int(options[i].get("lineNumber")),
            item_id=options[i].get("context"),
            level=int(options[i].get("level")),
            part_code=int(options[i].get("partCode")),
        )
        for i in selected_items
    ]

    return selected_sale_lines


def _get_options_to_apply_discount(
    pos_id,             # type: str
    sale_lines,         # type: eTree.Element
    discount_details,   # type: Dict[str, float]
    coupon,             # type: Dict[str, Any]
):
    # type: (...) -> List[Dict[str, Any]]

    options = {}
    item_level = {}
    line_number_status = {}

    groups_and_items_restrictions = _get_action_groups_and_items_restrictions(coupon)
    all_products = json.loads(getProducts(pos_id=pos_id))

    for sale_line in sale_lines:
        line_number = sale_line.get("lineNumber")
        context = sale_line.get("itemId")
        part_code = sale_line.get("partCode")
        item_price = float(sale_line.get("itemPrice", 0))
        product_name = sale_line.get("productName")
        parent_key = "{}.{}".format(
            line_number,
            context,
        )
        my_key = parent_key + "." + part_code

        not_cfh = all_products.get(part_code, {}).get("CFH") != "true"
        qty = float(sale_line.get("qty", 0))
        if qty > 0 and not_cfh and sale_line.get("itemType") != "OPTION":
            level = sale_line.get("level")
            sale_line_identifier = "{}.{}-{}.{}".format(
                line_number,
                level,
                context,
                part_code,
            )
            item_total = item_price - discount_details.get(sale_line_identifier, 0)
            option = {
                "lineNumber": line_number,
                "level": level,
                "context": context,
                "partCode": part_code,
                "qty": qty,
                "productName": remove_accents(text=product_name),
                "itemPrice": item_price,
                "itemTotal": "{:.3f}".format(item_total),
                "status": _get_sale_line_status(
                    groups_and_items_restrictions=groups_and_items_restrictions,
                    sale_line=sale_line,
                    item_total=item_total,
                )
            }

            if line_number not in options:
                options[line_number] = []
                line_number_status[line_number] = False

            options[line_number].append(option)
            line_number_status[line_number] |= option.get("status") == "enabled"

            item_level[my_key] = item_level.get(parent_key, -1) + 1
        else:
            item_level[my_key] = item_level.get(parent_key, -1)

    filtered_options = _filter_line_number_all_disabled(
        line_number_status=line_number_status,
        options=options,
    )

    return filtered_options


def _filter_line_number_all_disabled(
    line_number_status,     # type: Dict
    options,                # type: Dict
):
    # type: (...) -> List

    filtered_options = []
    for level, values in options.items():
        if line_number_status[level]:
            filtered_options.extend(values)

    return filtered_options


def _get_sale_line_status(
    groups_and_items_restrictions,      # type: Dict[str, Any]
    sale_line,                          # type: eTree.Element
    item_total,                         # type: float
):
    # type: (...) -> str

    if item_total == 0:
        return "disabled"

    groups = groups_and_items_restrictions["groups"]
    items = groups_and_items_restrictions["items"]

    if not groups and not items:
        return "enabled"

    if groups:
        sale_line_groups = json.loads(sale_line.get("jsonArrayTags", "[]"))
        valid_group = any(["PLUFamilyGroup={}".format(group) in sale_line_groups for group in groups])
        if valid_group:
            return "enabled"

    if sale_line.get("partCode") in items:
        return "enabled"

    for part_code in sale_line.get("itemId").split(".")[1:]:
        if part_code in items:
            return "enabled"

    return "disabled"


def _get_discount_details(
    order,  # type: eTree.Element
):
    # type: (...) -> Dict[str, float]

    benefit_list = order.find(".//CustomOrderProperties/OrderProperty[@key='BENEFIT_LIST']")
    benefit_list = json.loads(benefit_list.get("value") or "{}") if benefit_list is not None else {}
    discount_values = benefit_list.get("discountItemValues", {})

    discount_details = {}
    if not discount_values:
        return discount_details

    for _, value in discount_values.items():
        for sale_line_key, amount in value.items():
            if sale_line_key in discount_details:
                discount_details[sale_line_key] += amount
            else:
                discount_details[sale_line_key] = amount

    return discount_details


def _get_action_groups_and_items_restrictions(
    coupon,     # type: Dict[str, Any]
):
    # type: (...) -> Dict[str, List]

    groups_and_items_restrictions = {
        "groups": [],
        "items": [],
    }
    if not coupon.get("actions") or not coupon.get("actions").get("0"):
        return groups_and_items_restrictions

    items_actions = [
        "discountPercentageByItems",
        "discountAmountByItems",
        "discountPercentageByItemsOnTotal",
        "discountAmountByItemsOnTotal",
    ]
    groups_and_items_restrictions["items"].extend(
        _populate_groups_and_items_restrictions(
            coupon=coupon,
            actions=items_actions,
            event_key="item",
        )
    )

    group_actions = [
        "discountPercentageByGroups",
        "discountAmountByGroups",
        "discountPercentageByGroupsOnTotal",
        "discountAmountByGroupsOnTotal",
    ]
    groups_and_items_restrictions["groups"].extend(
        _populate_groups_and_items_restrictions(
            coupon=coupon,
            actions=group_actions,
            event_key="group",
        )
    )

    return groups_and_items_restrictions


def _populate_groups_and_items_restrictions(
    coupon,         # type: Dict[str, Any]
    actions,        # type: List[str]
    event_key,      # type: str
):
    # type: (...) -> List

    action = coupon.get("actions").get("0").items()[0]
    action_name = action[0]
    if action_name not in actions:
        return []

    restictions = []
    action_events = action[1]
    for event in action_events:
        restictions.append(event.get(event_key))

    return restictions


def _show_multiple_select_list_box_dialog(
    pos_id,     # type: str
    options,    # type: Union[str, List[str]]
    title="",   # type: str
):
    # type: (...) -> Optional[List[int]]

    if isinstance(options, list):
        options = "|".join(options)

    response = show_any_dialog(
        posid=pos_id,
        dlgtype="multipleSelectListBox",
        message="",
        title=title,
        infotext=options,
        buttons="$OK|$CANCEL",
        icon="",
        timeout=600000,
        mask="",
        defvalue="",
        minvalue="",
        maxvalue="",
        contents=None,
        asynch=False,
    )

    if response is None:
        return None

    return map(int, response.split(","))
