import json
import logging

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

from bustoken import TK_LOYALTY_GET_PAYMENT_BENEFIT
from msgbus import (
    TK_SYS_ACK,
    FM_PARAM,
)
from mw_helper import (
    format_currency,
    show_message_options_dialog,
)
from old_helper import round_half_away_from_zero
from posot import OrderTaker
from sysactions import (
    send_message,
    get_model,
    get_posot,
)

from actions.loyalty.model import (
    LoyaltyDiscountData,
    LoyaltyStopPaymentAction,
)
from actions.config import should_ask_loyalty_on_payment
from actions.discount.apply_discount import apply_discount
from application.interactor import LoyaltyInteractor
from application.domain import TenderIdEnum
from open_loyalty import get_user_input_by_type
from utilfunctions import get_benefit_total_value
from utilfunctions import (
    get_custom_property_value,
    value_is_close_to,
)

logger = logging.getLogger("PosActions")
loyalty_interactor = LoyaltyInteractor()


def apply_loyalty_discount_on_payment(
    pos_id,                 # type: str
    tender_info,            # type: Dict[str, Any]
    payment_amount,         # type: float
    order_due_amount,       # type: float
    order_taker,            # type: OrderTaker
):
    # type: (...) -> Optional[LoyaltyDiscountData]

    payment_info = _build_payment_info(
        amount=payment_amount,
        order_due=order_due_amount,
        tender_info=tender_info,
    )
    message_data = _build_message_data(
        order_taker=order_taker,
        pos_id=pos_id,
        payment_info=payment_info,
    )
    if not message_data:
        return None

    loyalty_id = message_data["customerId"]
    loyalty_title = "$LOYALTY_PROGRAM"
    try:
        response = send_message(
            dest_name="Loyalty",
            token=TK_LOYALTY_GET_PAYMENT_BENEFIT,
            format=FM_PARAM,
            data=json.dumps(message_data),
        )

        if not response or response.token != TK_SYS_ACK:
            _loyalty_failed_discount_message(
                pos_id=pos_id,
                loyalty_title=loyalty_title,
                message="$COMMUNICATION_FAILED",
            )
            return None

        if not response.data:
            _loyalty_failed_discount_message(
                pos_id=pos_id,
                loyalty_title=loyalty_title,
                message="$NO_LOYALTY_DISCOUNTS_FOUND",
            )
            return None

        coupon_json = json.loads(response.data)
        discount_amount = _get_discount_amount(
            payment_amount=payment_amount,
            coupon_json=coupon_json,
        )

        if discount_amount <= 0:
            _loyalty_failed_discount_message(
                pos_id=pos_id,
                loyalty_title=loyalty_title,
                message="$NO_LOYALTY_DISCOUNTS_FOUND",
            )
            return LoyaltyDiscountData(
                loyalty_id=loyalty_id,
                new_payment_amount=payment_amount,
                discount_amount=0,
            )

        model = get_model(posid=pos_id)
        selected_coupon = _get_selected_coupon(
            coupon_json=coupon_json,
            pos_id=pos_id,
            loyalty_title=loyalty_title,
            discount_amount=discount_amount,
            model=model,
        )

        if not selected_coupon:
            return LoyaltyDiscountData(
                loyalty_id=loyalty_id,
                new_payment_amount=payment_amount,
                discount_amount=0,
            )

        order_picture = _apply_selected_benefit(
            pos_id=pos_id,
            selected_coupon=selected_coupon,
            payment_info=payment_info,
        )

        if order_picture is None:
            return None

        return _build_loyalty_data(
            loyalty_id=message_data.get("customerId"),
            benefit_id=selected_coupon.get("benefit_id"),
            order_picture=order_picture,
            payment_amount=payment_amount,
            payment_info=payment_info,
        )
    except LoyaltyStopPaymentAction as ex:
        raise ex
    except (Exception, ) as _:
        logger.exception("Error searching for loyalty discounts on payment")


def _build_loyalty_data(
    loyalty_id,         # type: str
    benefit_id,         # type: str
    order_picture,      # type: eTree.Element
    payment_amount,     # type: float
    payment_info,       # type: Dict[str, Any]
):
    # type: (...) -> LoyaltyDiscountData

    benefit_total_value = get_benefit_total_value(
        benefit_id=benefit_id,
        order_picture=order_picture,
    )
    new_payment_amount = max(0.0, payment_amount - benefit_total_value)

    due_amount = float(order_picture.get("dueAmount"))
    if payment_info.get("tenderId") == TenderIdEnum.CASH.value and payment_amount > due_amount > 0:
        new_payment_amount = payment_amount

    amount_equals_to_zero = value_is_close_to(
        value=new_payment_amount,
        close_to=0.0,
    )
    if amount_equals_to_zero:
        new_payment_amount = 0
    else:
        new_payment_amount = round_half_away_from_zero(
            number=new_payment_amount,
            precision=2,
        )

    return LoyaltyDiscountData(
        new_payment_amount=new_payment_amount,
        discount_amount=benefit_total_value,
        benefit_id=benefit_id,
        loyalty_id=loyalty_id,
    )


def _build_message_data(
    order_taker,    # type: OrderTaker
    pos_id,         # type: str
    payment_info,   # type: Dict[str, Union[str, float, int]]
):
    # type: (...) -> Optional[Dict[str, Any]]

    if not should_ask_loyalty_on_payment():
        return None

    customer_id = _get_customer_id(
        pos_id=pos_id,
        order_taker=order_taker,
    )
    if not customer_id:
        return None

    return {
        "posId": pos_id,
        "customerId": customer_id,
        "paymentPicture": json.dumps(payment_info).encode("utf-8")
    }


def _build_payment_info(
    amount,         # type: float
    order_due,      # type: float
    tender_info,    # type: Dict[str, Any]
):
    # type: (...) -> Dict[str, Union[str, float, int]]

    payment_picture = {
        "tenderId": tender_info["id"],
        "tenderAmount": amount,
        "tenderDescr": tender_info["descr"],
        "tenderType": str(tender_info.get("electronicTypeId", 0)),
        "changeAmount": max(0.0, float(amount) - order_due),
    }
    return payment_picture


def _get_discount_amount(
    payment_amount,     # type: float
    coupon_json,        # type: Dict[str, Any]
):
    # type: (...) -> Optional[float]

    discount_amount = None

    for action in coupon_json.get("actions").values():
        if "discountAmountOnTotal" in action.keys():
            discount_amount = min(payment_amount, action.get("discountAmountOnTotal"))
            action["discountAmountOnTotal"] = discount_amount
            break

        if "paymentAmount" in action.keys():
            discount_amount = min(payment_amount, action.get("paymentAmount")[0].get("amount"))
            action["paymentAmount"][0]["amount"] = discount_amount
            break

    return discount_amount


def _loyalty_failed_discount_message(
    pos_id,             # type: str
    loyalty_title,      # type: str
    message,            # type: str
):
    # type: (...) -> None

    ret = show_message_options_dialog(
        pos_id=pos_id,
        buttons="$OK|$CANCEL_PAYMENT",
        message=message,
        title=loyalty_title,
    )
    if ret == 1:
        raise LoyaltyStopPaymentAction()


def _get_selected_coupon(
    coupon_json,        # type: Dict[str, Any]
    pos_id,             # type: str
    loyalty_title,      # type: str
    discount_amount,    # type: float
    model,              # type: eTree.XML
):
    # type: (...) -> Optional[Dict[str, Any]]

    formatted_discount = format_currency(
        model=model,
        value=discount_amount,
    )
    ret = show_message_options_dialog(
        pos_id=pos_id,
        buttons="$OK|$DONT_USE|$CANCEL_PAYMENT",
        message="$WANT_TO_APPLY_THE_DISCOUNT|{}".format(formatted_discount),
        title=loyalty_title,
    )
    if ret == 2:
        raise LoyaltyStopPaymentAction()

    if ret == 1:
        return None

    benefit_id = coupon_json.get("id")
    name = coupon_json.get("name")
    selected_coupon = {
        "coupon": coupon_json,
        "benefit_id": benefit_id,
        "name": name,
    }

    return selected_coupon


def _apply_selected_benefit(
    selected_coupon,    # type: Dict[str, Any]
    pos_id,             # type: str
    payment_info,       # type: Dict[str, Any]
):
    # type: (...) -> Optional[eTree.Element]

    resp = apply_discount(
        pos_id=pos_id,
        selected_coupon=selected_coupon,
        payment_info=payment_info,
    )
    if not resp:
        return None

    model = get_model(posid=pos_id)
    pos_ot = get_posot(model=model)
    return eTree.XML(pos_ot.orderPicture(posid=pos_id))


def _get_customer_id(
    pos_id,         # type: str
    order_taker,    # type: OrderTaker
):
    # type: (...) -> Optional[str]

    customer_id = get_custom_property_value(
        pos_id=pos_id,
        custom_property="LOYALTY_ID",
    )

    if customer_id:
        return None

    loyalty_type_id = loyalty_interactor.get_loyalty_id_type()
    customer_id = get_user_input_by_type(
        pos_id=pos_id,
        pos_ot=order_taker,
        loyalty_type_id=loyalty_type_id,
        default_doc="",
        stop_action=False,
    )

    return customer_id
