"""Price a cart against a published Nexus menu, applying the same checks as the gateway.

Amounts are integer bani (3700 = 37.00 RON). Unavailable or invalid selections raise
CartError with the conflict code the gateway would return.
"""


class CartError(Exception):
    def __init__(self, code):
        super().__init__(code)
        self.code = code


def price_cart(menu, fulfillment, items, tip=0, checkout_discount=None):
    """Return the order total in bani for `items` (the order's items array)."""
    if not menu.get("accepting_orders", True):
        raise CartError("restaurant_closed")
    if type(tip) is not int or not 0 <= tip <= 100_000_000:
        raise CartError("invalid_request")
    if tip and not menu.get("tips_enabled", False):
        raise CartError("tips_unavailable")
    products = {product["id"]: product for product in menu["products"]}
    total = tip + (menu.get("delivery_fee", 0) if fulfillment == "delivery" else 0)
    for line in items:
        total += unit_price(products.get(line["product_id"]), line.get("modifiers", [])) * line["quantity"]
    if checkout_discount is not None:
        scope = checkout_discount.get("product_ids")
        ids = {line["product_id"] for line in items}
        amount = checkout_discount.get("amount")
        if (type(amount) is not int or not 1 <= amount <= 100_000_000
                or scope is not None and (not scope or len(set(scope)) != len(scope) or not set(scope) <= ids)):
            raise CartError("invalid_checkout_discount")
        eligible = sum(unit_price(products[line["product_id"]], line.get("modifiers", []), True) * line["quantity"]
                       for line in items if scope is None or line["product_id"] in scope)
        if amount > eligible:
            raise CartError("invalid_checkout_discount")
        total -= amount
    return total


def unit_price(product, modifiers, base_only=False):
    if product is None or not product.get("available", True):
        raise CartError("product_unavailable")
    selected = {(choice["group_id"], choice["option_id"]) for choice in modifiers}
    if len(selected) != len(modifiers):
        raise CartError("duplicate_modifier")
    groups = product.get("modifier_groups", [])
    if not selected <= {(group["id"], option["id"]) for group in groups for option in group["options"]}:
        raise CartError("unknown_modifier")

    # A product with variants is priced by the one variant matching all "variant" choices.
    deposit = product.get("deposit", 0)
    unit = product["price"] - product.get("discount", 0)
    if product.get("variants"):
        defining_groups = {group["id"] for group in groups if group.get("kind", "extra") == "variant"}
        defining = {choice for choice in selected if choice[0] in defining_groups}
        variant = next((variant for variant in product["variants"]
                        if {(s["group_id"], s["option_id"]) for s in variant["selections"]} == defining), None)
        if variant is None or not variant.get("available", True):
            raise CartError("variant_unavailable")
        deposit = variant.get("deposit", 0)
        unit = variant["price"] - variant.get("discount", 0)
    if base_only:
        return unit
    unit += deposit + product.get("packaging", 0)

    # Every group enforces its bounds; selected options add their price and deposit.
    for group in groups:
        chosen = [option for option in group["options"] if (group["id"], option["id"]) in selected]
        if not group.get("minimum", 0) <= len(chosen) <= group.get("maximum", 1):
            raise CartError("modifier_selection_invalid")
        if any(not option.get("available", True) for option in chosen):
            raise CartError("modifier_unavailable")
        unit += sum(option["price"] + option.get("deposit", 0) for option in chosen)
    return unit
