import logging
from uuid import uuid4

from django.db import transaction
from django.http import Http404, HttpResponse
from django.shortcuts import redirect
from django.urls import reverse
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import View

from mollie.api.error import Error as MollieApiError
from oscar.apps.order.exceptions import InvalidPaymentEvent
from oscar.core.loading import get_class, get_model

from ocyan.mollie.provider import MollieProviderError
from ocyan.mollie.webhooks import event_fingerprint, notification_reference

from .mollie_payment import MolliePayment
from .models import MollieWebhookEvent

Order = get_model("order", "Order")
Source = get_model("payment", "Source")
ThankYouFromLinkView = get_class("checkout.views", "ThankYouFromLinkView")
logger = logging.getLogger("payment_mollie")


class MolliePaymentView(View):
    method = None

    def get(self, request):
        method = self.method
        order = Order.objects.get(
            pk=request.session["checkout_order_id"]
        )  # pylint: disable=W0212

        oidb64 = urlsafe_base64_encode(force_bytes(order.pk))
        token = order.verification_hash()

        redirect_url = request.build_absolute_uri(
            reverse(
                "payment_mollie:mollie_callback",
                kwargs={"oidb64": oidb64, "token": token},
            )
        )

        webhook_url = request.build_absolute_uri(reverse("payment_mollie:webhook"))
        attempt_key = "mollie_checkout_attempt:%s:%s" % (order.number, method)
        idempotency_key = request.session.get(attempt_key)
        if not idempotency_key:
            idempotency_key = str(uuid4())
            request.session[attempt_key] = idempotency_key
        mollie_payment = MolliePayment()
        payment_url = mollie_payment.create_payment(
            order,
            method,
            webhook_url,
            redirect_url,
            idempotency_key=idempotency_key,
        )
        return redirect(payment_url)


class MollieCallbackView(ThankYouFromLinkView):
    """
    The order will be verified and set through the webhook.
    """

    def get(self, request, oidb64, token):  # pylint: disable=W0613
        order = self.get_object(oidb64, token)
        mollie_payment = MolliePayment()
        cancel = False

        try:
            source = (
                Source.objects.filter(
                    order=order, source_type__code__startswith="mollie:"
                )
                .order_by("-id")
                .first()
            )
            if source is None:
                raise Source.DoesNotExist
        except Source.DoesNotExist:
            return redirect(reverse("checkout:try-again"))

        if source.reference.startswith("ord_"):
            payment = mollie_payment.mollie.orders.get(source.reference)
            if payment.is_created():
                # status is created when the user clicks on the "go back" button in klarna
                cancel = True
        else:
            payment = mollie_payment.mollie.payments.get(source.reference)
            if payment.is_failed():
                cancel = True

        if cancel or payment.is_expired() or payment.is_canceled():
            mollie_payment.cancel_payment_event(order, payment.id)
            return redirect(reverse("checkout:try-again"))

        try:
            MolliePayment().update_payment_status(payment=payment)
        except InvalidPaymentEvent:
            # The current payment status equals the new one. Exception happens because a duplicate event gets created
            pass

        if payment.is_expired():
            return redirect(reverse("checkout:cancel"))

        return redirect(order.get_thank_you_url())


@method_decorator(csrf_exempt, name="dispatch")
class WebhookView(View):
    """
    Handle a Mollie payment status change.

    When the payment is verified and completed send the user a email.
    If the order has failed for some reason, send the admin & user a email.

    Since the pipeline changes (apparently) for each project we need to
    find a way to get the current used pipeline (with their statuses) and
    handle on that. It's a empty list for now.
    """

    def post(self, request):
        try:
            payment_id = notification_reference(request.POST)
        except MollieProviderError:
            # A malformed or unsolicited classic webhook must not disclose any
            # order information. Mollie recommends acknowledging unknown IDs.
            return HttpResponse(status=200)

        with transaction.atomic():
            mollie_payment = MolliePayment()
            try:
                if payment_id.startswith("ord_"):
                    payment = mollie_payment.mollie.orders.get(payment_id)
                else:
                    payment = mollie_payment.mollie.payments.get(payment_id)
                status = str(payment.get("status") or "unknown")[:32]
                fingerprint = event_fingerprint(payment)
                webhook_event, created = MollieWebhookEvent.objects.get_or_create(
                    provider_reference=payment_id,
                    fingerprint=fingerprint,
                    defaults={"status": status},
                )
                if not created and webhook_event.processed_at:
                    return HttpResponse(status=200)
                mollie_payment.update_payment_status(payment=payment)
                webhook_event.processed_at = timezone.now()
                webhook_event.failure_reason = ""
                webhook_event.save(update_fields=("processed_at", "failure_reason"))
            except Http404:
                # Unknown references must be acknowledged after the remote
                # status fetch; this does not reveal local order existence.
                return HttpResponse(status=200)
            except InvalidPaymentEvent:
                # Whenever a system calls the webhook and dies it will call this function
                # multiple times. Instead of settings the handle again, just die
                logger.error(
                    "InvalidPaymentEvent got raised. PaymentEventType is proberly already set for %s",
                    payment_id,
                )
            except (MollieProviderError, MollieApiError) as exc:
                logger.warning("Mollie webhook could not be processed: %s", exc)
                return HttpResponse(status=500)

        return HttpResponse(status=200)
