from collections import defaultdict
from decimal import Decimal
from django import forms
from django.dispatch import receiver
from django.http import HttpRequest
from django.urls import resolve, reverse
from django.utils.translation import gettext, gettext_lazy as _
from pretix.base.decimal import round_decimal
from pretix.base.models import CartPosition, Event, TaxRule
from pretix.base.models.orders import OrderFee
from pretix.base.settings import settings_hierarkey
from pretix.base.signals import (
    event_copy_data,
    item_copy_data,
    order_fee_calculation,
)
from pretix.base.templatetags.money import money_filter
from pretix.control.signals import item_forms, nav_event_settings
from pretix.presale.signals import (
    fee_calculation_for_cart,
    front_page_top,
    order_meta_from_request,
)
from pretix.presale.views import get_cart
from pretix.presale.views.cart import cart_session

from .models import ItemServicefeesSettings


@receiver(nav_event_settings, dispatch_uid="service_fee_nav_settings")
def navbar_settings(sender, request, **kwargs):
    url = resolve(request.path_info)
    return [
        {
            "label": _("Service Fee"),
            "url": reverse(
                "plugins:pretix_servicefees:settings",
                kwargs={
                    "event": request.event.slug,
                    "organizer": request.organizer.slug,
                },
            ),
            "active": url.namespace == "plugins:pretix_servicefees"
            and url.url_name.startswith("settings"),
        }
    ]


def get_fees(
    event,
    total,
    invoice_address,
    mod="",
    request=None,
    positions=[],
    gift_cards=None,
    payment_requests=None,
):
    if request is not None and not positions:
        positions = get_cart(request)

    skip_free = event.settings.get("service_fee_skip_free", as_type=bool)
    if skip_free:
        positions = [pos for pos in positions if pos.price != Decimal("0.00")]

    skip_addons = event.settings.get("service_fee_skip_addons", as_type=bool)
    if skip_addons:
        positions = [pos for pos in positions if not pos.addon_to_id]

    excluded_products = set(
        ItemServicefeesSettings.objects.filter(
            item__event=event,
            exclude=True,
        ).values_list("item_id", flat=True)
    )
    positions = [pos for pos in positions if pos.item_id not in excluded_products]

    skip_non_admission = event.settings.get(
        "service_fee_skip_non_admission", as_type=bool
    )
    if skip_non_admission:
        positions = [pos for pos in positions if pos.item.admission]

    fee_per_ticket = event.settings.get("service_fee_per_ticket" + mod, as_type=Decimal)
    if mod and fee_per_ticket is None:
        fee_per_ticket = event.settings.get("service_fee_per_ticket", as_type=Decimal)

    fee_abs = event.settings.get("service_fee_abs" + mod, as_type=Decimal)
    if mod and fee_abs is None:
        fee_abs = event.settings.get("service_fee_abs", as_type=Decimal)

    fee_percent = event.settings.get("service_fee_percent" + mod, as_type=Decimal)
    if mod and fee_percent is None:
        fee_percent = event.settings.get("service_fee_percent", as_type=Decimal)

    fee_per_ticket = Decimal("0") if fee_per_ticket is None else fee_per_ticket
    fee_abs = Decimal("0") if fee_abs is None else fee_abs
    fee_percent = Decimal("0") if fee_percent is None else fee_percent

    if event.settings.get("service_fee_skip_if_gift_card", as_type=bool):
        if payment_requests is not None:
            for p in payment_requests:
                if p["provider"] == "giftcard":
                    total = max(0, total - Decimal(p["max_value"] or "0"))

        else:
            # pretix pre 4.15
            gift_cards = gift_cards or []
            if request:
                cs = cart_session(request)
                if cs.get("gift_cards"):
                    gift_cards = event.organizer.accepted_gift_cards.filter(
                        pk__in=cs.get("gift_cards"), currency=event.currency
                    )
            summed = 0
            for gc in gift_cards:
                fval = Decimal(gc.value)  # TODO: don't require an extra query
                fval = min(fval, total - summed)
                if fval > 0:
                    total -= fval
                    summed += fval

    # This hack allows any payment provider to declare a setting service_fee_skip_if_{pprovname} in order to implement
    # the skipping of service fees when they are paid in their entirety through said payment provider/method.
    # It is notably used by the KulturPass, which is for all intents and purposes a giftcard but handled like a regular
    # payment provider.
    if payment_requests is not None:
        for p in payment_requests:
            if event.settings.get(
                f'service_fee_skip_if_{p["provider"]}', default=False, as_type=bool
            ):
                total = max(0, total - Decimal(p["max_value"] or "0"))

    if (fee_per_ticket or fee_abs or fee_percent) and total != Decimal("0.00"):
        tax_rule_zero = TaxRule.zero()
        fee = round_decimal(
            fee_abs + total * (fee_percent / 100) + len(positions) * fee_per_ticket,
            event.currency,
        )
        split_taxes = event.settings.get("service_fee_split_taxes", as_type=bool)
        if split_taxes:
            # split taxes based on products ordered
            d = defaultdict(lambda: Decimal("0.00"))
            for p in positions:
                if isinstance(p, CartPosition):
                    tr = p.item.tax_rule
                else:
                    tr = p.tax_rule
                d[tr] += p.price - p.tax_value

            base_values = sorted([tuple(t) for t in d.items()], key=lambda t: t[0].rate)
            sum_base = sum(t[1] for t in base_values)
            if sum_base:
                fee_values = [
                    (t[0], round_decimal(fee * t[1] / sum_base, event.currency))
                    for t in base_values
                ]
                sum_fee = sum(t[1] for t in fee_values)

                # If there are rounding differences, we fix them up, but always leaning to the benefit of the tax
                # authorities
                if sum_fee > fee:
                    fee_values[0] = (
                        fee_values[0][0],
                        fee_values[0][1] + (fee - sum_fee),
                    )
                elif sum_fee < fee:
                    fee_values[-1] = (
                        fee_values[-1][0],
                        fee_values[-1][1] + (fee - sum_fee),
                    )
            else:
                fee_values = [(event.settings.tax_rate_default or tax_rule_zero, fee)]

        else:
            fee_values = [(event.settings.tax_rate_default or tax_rule_zero, fee)]

        fees = []
        for tax_rule, price in fee_values:
            tax_rule = tax_rule or tax_rule_zero
            tax = tax_rule.tax(
                price, invoice_address=invoice_address, base_price_is="gross"
            )
            fees.append(
                OrderFee(
                    fee_type=OrderFee.FEE_TYPE_SERVICE,
                    internal_type="",
                    value=price,
                    tax_rate=tax.rate,
                    tax_code=tax.code,
                    tax_value=tax.tax,
                    tax_rule=tax_rule,
                )
            )
        return fees

    return []


@receiver(fee_calculation_for_cart, dispatch_uid="service_fee_calc_cart")
def cart_fee(sender: Event, request: HttpRequest, invoice_address, total, **kwargs):
    mod = ""
    try:
        from pretix_resellers.utils import (
            ResellerException,
            get_reseller_and_user,
        )
    except ImportError:
        pass
    else:
        try:
            reseller, user = get_reseller_and_user(request)
            config = reseller.configs.get(organizer_id=sender.organizer_id)
            if config.skip_default_service_fees:
                return []
        except ResellerException:
            pass
        else:
            mod = "_resellers"
    return get_fees(
        sender,
        total,
        invoice_address,
        mod,
        request,
        payment_requests=kwargs.get("payment_requests"),
    )


@receiver(order_fee_calculation, dispatch_uid="service_fee_calc_order")
def order_fee(
    sender: Event, positions, invoice_address, total, meta_info, gift_cards, **kwargs
):
    mod = ""
    if meta_info.get("servicefees_reseller_id"):
        mod = "_resellers"
        try:
            from pretix_resellers.models import Reseller
        except ImportError:
            pass
        else:
            r = Reseller.objects.get(pk=meta_info.get("servicefees_reseller_id"))
            config = r.configs.get(organizer_id=sender.organizer_id)
            if config.skip_default_service_fees:
                return []

    return get_fees(
        sender,
        total,
        invoice_address,
        mod,
        positions=positions,
        gift_cards=gift_cards,
        payment_requests=kwargs.get("payment_requests"),
    )


@receiver(front_page_top, dispatch_uid="service_fee_front_page_top")
def front_page_top_recv(sender: Event, **kwargs):
    fees = []
    fee_per_ticket = sender.settings.get("service_fee_per_ticket", as_type=Decimal)
    if fee_per_ticket:
        fees = fees + [
            "{} {}".format(
                money_filter(fee_per_ticket, sender.currency), gettext("per ticket")
            )
        ]

    fee_abs = sender.settings.get("service_fee_abs", as_type=Decimal)
    if fee_abs:
        fees = fees + [
            "{} {}".format(money_filter(fee_abs, sender.currency), gettext("per order"))
        ]

    fee_percent = sender.settings.get("service_fee_percent", as_type=Decimal)
    if fee_percent:
        fees = fees + ["{} % {}".format(fee_percent, gettext("per order"))]

    if fee_per_ticket or fee_abs or fee_percent:
        return "<p>%s</p>" % gettext(
            "A service fee of {} will be added to the order total."
        ).format(" {} ".format(gettext("plus")).join(fees))


@receiver(order_meta_from_request, dispatch_uid="servicefees_order_meta")
def order_meta_signal(sender: Event, request: HttpRequest, **kwargs):
    meta = {}
    try:
        from pretix_resellers.utils import (
            ResellerException,
            get_reseller_and_user,
        )
    except ImportError:
        pass
    else:
        try:
            reseller, user = get_reseller_and_user(request)
            meta["servicefees_reseller_id"] = reseller.pk
        except ResellerException:
            pass
    return meta


class ItemServicefeesSettingsForm(forms.ModelForm):
    class Meta:
        model = ItemServicefeesSettings
        fields = ["exclude"]
        exclude = []

    def __init__(self, *args, **kwargs):
        self.event = kwargs.pop("event")
        super().__init__(*args, **kwargs)

    def save(self, commit=True):
        if not self.cleaned_data.get("exclude"):
            if self.instance.pk:
                self.instance.delete()
            else:
                return
        else:
            return super().save(commit=commit)


@receiver(item_forms, dispatch_uid="servicefees_item_forms")
def control_item_forms(sender, request, item, **kwargs):
    try:
        inst = ItemServicefeesSettings.objects.get(item=item)
    except ItemServicefeesSettings.DoesNotExist:
        inst = ItemServicefeesSettings(item=item)
    return ItemServicefeesSettingsForm(
        instance=inst,
        event=sender,
        data=(request.POST if request.method == "POST" else None),
        prefix="servicefees",
    )


@receiver(item_copy_data, dispatch_uid="servicefees_item_copy")
def copy_item(sender, source, target, **kwargs):
    try:
        inst = ItemServicefeesSettings.objects.get(item=source)
        inst = copy.copy(inst)
        inst.pk = None
        inst.item = target
        inst.save()
    except ItemServicefeesSettings.DoesNotExist:
        pass


@receiver(signal=event_copy_data, dispatch_uid="servicefees_copy_data")
def event_copy_data_receiver(sender, other, question_map, item_map, **kwargs):
    for ip in ItemServicefeesSettings.objects.filter(item__event=other):
        ip = copy.copy(ip)
        ip.pk = None
        ip.event = sender
        ip.item = item_map[ip.item_id]
        ip.save()


settings_hierarkey.add_default("service_fee_skip_addons", "True", bool)
settings_hierarkey.add_default("service_fee_skip_free", "True", bool)