"""
Coupon rules in one place, used by the shop cart, digital magazine unlocks and game rewards.

    lines = [(product, unit_price, quantity, 'print' | 'digital'), ...]
    result = check(code, lines, user=request.user, phone='017…')   # raises CouponError
    redeem(result, order=order)                                    # inside the purchase
"""
import re
from dataclasses import dataclass
from decimal import ROUND_HALF_UP, Decimal

from django.db import transaction
from django.db.models import F, Q

from .models import Coupon, CouponRedemption

TWO = Decimal('0.01')


class CouponError(Exception):
    pass


@dataclass
class Result:
    coupon: Coupon
    discount: Decimal
    eligible_total: Decimal
    user: object = None
    phone: str = ''


def _phone(value):
    digits = re.sub(r'\D', '', value or '')
    return digits[2:] if digits.startswith('880') else digits


def _authed(user):
    return user is not None and getattr(user, 'is_authenticated', False)


def used_by(coupon, user=None, phone=''):
    q = Q()
    if _authed(user):
        q |= Q(user=user)
    if phone:
        q |= Q(phone=phone)
    return coupon.redemptions.filter(q).count() if q else 0


def check(code, lines, user=None, phone=''):
    code = (code or '').strip().upper()
    if not code:
        raise CouponError('Enter a coupon code.')
    coupon = Coupon.objects.filter(code__iexact=code).first()
    if not coupon:
        raise CouponError('This coupon code does not exist.')
    if not coupon.is_valid():
        raise CouponError('This coupon has expired or has been fully used.')
    if (coupon.requires_login or coupon.owner_id) and not _authed(user):
        raise CouponError('Please sign in to use this coupon.')
    if coupon.owner_id and coupon.owner_id != user.pk:
        raise CouponError('This coupon belongs to another account.')
    phone = _phone(phone)
    if coupon.per_customer_limit is not None and used_by(coupon, user, phone) >= coupon.per_customer_limit:
        raise CouponError('You have already used this coupon.')
    eligible = sum((Decimal(price) * qty for product, price, qty, channel in lines if coupon.covers(product, channel)),
                   Decimal('0'))
    if eligible <= 0:
        raise CouponError(f'This coupon is not valid for these items ({coupon.get_applies_to_display()}).')
    if coupon.min_order_amount and eligible < coupon.min_order_amount:
        raise CouponError(f'This coupon needs a minimum order of ৳{coupon.min_order_amount:.0f}.')
    if coupon.discount_type == 'percentage':
        discount = eligible * coupon.discount_value / Decimal('100')
    else:
        discount = coupon.discount_value
    if coupon.max_discount is not None:
        discount = min(discount, coupon.max_discount)
    discount = min(discount, eligible).quantize(TWO, rounding=ROUND_HALF_UP)
    return Result(coupon, discount, eligible, user if _authed(user) else None, phone)


def redeem(result, order=None, digital_access=None):
    """Count the use. Call inside transaction.atomic(); re-checks the total limit under a row lock."""
    with transaction.atomic():
        c = Coupon.objects.select_for_update().get(pk=result.coupon.pk)
        if c.max_uses is not None and c.current_uses >= c.max_uses:
            raise CouponError('This coupon was just used up.')
        Coupon.objects.filter(pk=c.pk).update(current_uses=F('current_uses') + 1)
        CouponRedemption.objects.create(coupon=c, user=result.user, phone=result.phone, order=order,
                                        digital_access=digital_access, amount=result.discount)
