"""
Promo code validation and application helpers.
"""
from decimal import Decimal

from django.db import transaction
from django.db.models import F

from apps.promotions.models import (
    APPLIES_CERTS,
    APPLIES_COURSES,
    PromoCode,
    PromoRedemption,
)


class PromoError(ValueError):
    pass


def lookup_promo(code: str) -> PromoCode:
    code = (code or '').strip().upper().replace(' ', '')
    if not code:
        raise PromoError('Enter a promo code.')
    try:
        return PromoCode.objects.get(code__iexact=code)
    except PromoCode.DoesNotExist:
        raise PromoError('Invalid promo code.')


def _check_user_limit(promo: PromoCode, user):
    if not promo.max_uses_per_user:
        return
    used = PromoRedemption.objects.filter(promo=promo, user=user).count()
    if used >= promo.max_uses_per_user:
        raise PromoError('You have already used this promo code the maximum times.')


def validate_promo_for_user(promo: PromoCode, user, scope: str, amount) -> PromoCode:
    if not promo.is_currently_valid:
        raise PromoError('This promo code is expired, inactive, or fully used.')
    if not promo.applies_to_scope(scope):
        if scope == APPLIES_COURSES:
            raise PromoError('This promo code cannot be used on courses.')
        raise PromoError('This promo code cannot be used on certifications.')

    amount = Decimal(amount or 0)
    if amount < Decimal(promo.min_amount or 0):
        raise PromoError(f'This promo requires a minimum amount of {promo.min_amount}.')

    _check_user_limit(promo, user)
    return promo


def apply_promo_amounts(promo: PromoCode, amount) -> tuple[Decimal, Decimal]:
    return promo.calculate_discount(amount)


@transaction.atomic
def apply_promo_to_enrollment(enrollment, code: str):
    promo = lookup_promo(code)
    base = Decimal(enrollment.course.price or 0)
    validate_promo_for_user(promo, enrollment.student, APPLIES_COURSES, base)

    discount, final = apply_promo_amounts(promo, base)
    if discount <= 0:
        raise PromoError('This promo does not reduce the current course fee.')

    enrollment.promo_code = promo
    enrollment.original_amount = base
    enrollment.discount_amount = discount
    enrollment.payable_amount = final
    enrollment.save(update_fields=[
        'promo_code', 'original_amount', 'discount_amount', 'payable_amount',
    ])
    return enrollment


def clear_promo_from_enrollment(enrollment):
    enrollment.promo_code = None
    enrollment.original_amount = None
    enrollment.discount_amount = Decimal('0')
    enrollment.payable_amount = None
    enrollment.save(update_fields=[
        'promo_code', 'original_amount', 'discount_amount', 'payable_amount',
    ])
    return enrollment


def enrollment_payable(enrollment) -> Decimal:
    if enrollment.payable_amount is not None:
        return Decimal(enrollment.payable_amount)
    return Decimal(enrollment.course.price or 0)


@transaction.atomic
def apply_promo_to_cert_application(application, code: str):
    promo = lookup_promo(code)
    base = Decimal(application.current_fee_due() or 0)
    validate_promo_for_user(promo, application.candidate, APPLIES_CERTS, base)

    discount, final = apply_promo_amounts(promo, base)
    if discount <= 0:
        raise PromoError('This promo does not reduce the current certification fee.')

    application.promo_code = promo
    application.original_amount = base
    application.discount_amount = discount
    application.payable_amount = final
    application.save(update_fields=[
        'promo_code', 'original_amount', 'discount_amount', 'payable_amount',
    ])
    return application


def clear_promo_from_cert_application(application):
    application.promo_code = None
    application.original_amount = None
    application.discount_amount = Decimal('0')
    application.payable_amount = None
    application.save(update_fields=[
        'promo_code', 'original_amount', 'discount_amount', 'payable_amount',
    ])
    return application


def cert_payable(application) -> Decimal:
    if application.payable_amount is not None:
        return Decimal(application.payable_amount)
    return Decimal(application.current_fee_due() or 0)


@transaction.atomic
def record_promo_redemption(
    *,
    promo,
    user,
    scope,
    original,
    discount,
    final,
    enrollment=None,
    certification_application=None,
):
    if not promo:
        return None

    qs = PromoRedemption.objects.filter(promo=promo, user=user)
    if enrollment and qs.filter(enrollment=enrollment).exists():
        return qs.filter(enrollment=enrollment).first()
    if certification_application and qs.filter(
        certification_application=certification_application
    ).exists():
        return qs.filter(certification_application=certification_application).first()

    redemption = PromoRedemption.objects.create(
        promo=promo,
        user=user,
        scope=scope,
        enrollment=enrollment,
        certification_application=certification_application,
        original_amount=original,
        discount_amount=discount,
        final_amount=final,
    )
    PromoCode.objects.filter(pk=promo.pk).update(used_count=F('used_count') + 1)
    return redemption
