import logging
from decimal import Decimal as D

from django.http import Http404
from mollie.api.objects.order import Order as MollieOrder

from oscar.apps.payment.exceptions import UnableToTakePayment
from oscar.core.loading import get_class, get_model

from ocyan.plugin.oscar.pipeline import (
    PIPELINE_STATUS_CANCELLED,
    PIPELINE_STATUS_PAID,
    PIPELINE_STATUS_PENDING_PAYMENT,
    PIPELINE_STATUS_REFUND,
)
from ocyan.plugin.oscar.utils import create_or_record_payment_events
from ocyan.mollie.constants import BANKTRANSFER
from ocyan.mollie.provider import MollieClientSettings, create_client, payment_status

PaymentEventType = get_model("order", "PaymentEventType")
Order = get_model("order", "Order")
Source = get_model("payment", "Source")
SourceType = get_model("payment", "SourceType")

EventHandler = get_class("order.processing", "EventHandler")

logger = logging.getLogger(__name__)


def send_order_confirmation_message(*args, **kwargs):
    """Lazy compatibility wrapper for the optional communications plugin."""
    from ocyan.plugin.oscar_communications.order_extension.processing import (
        send_order_confirmation_message as send_message,
    )

    return send_message(*args, **kwargs)


class MolliePaymentMixin:
    def __init__(self, api_key):
        self.handler = EventHandler()
        self.mollie = create_client(MollieClientSettings(api_key=api_key))

    def get_mandate_status(self, client_id):
        # Make sure the customer has valid mandates.
        data = self.mollie.customers.get(client_id).mandates.list()
        status = [d["status"] for d in data]
        # If there’s at least one mandate with a status set to valid then continue
        return True if "valid" in status else False

    def get_order_with_method(self, payment_id, method):
        try:
            source_type = self.get_source(method)
            order = Order.objects.get(sources__reference=payment_id)

            # if there is a source with a specific refrence, but with an other source_type
            # then we want to update the type
            if not order.sources.filter(source_type=source_type).exists():
                order.sources.filter(reference=payment_id).update(
                    source_type=source_type
                )

        except Order.DoesNotExist:
            raise Http404("Order with transaction %s not found" % payment_id)
        return order

    def update_payment_status(self, payment):
        """
        The Mollie payment status has changed.
        Translate this status update to Oscar.
        """

        if isinstance(payment, str):
            if payment.startswith("ord_"):
                payment = self.mollie.orders.get(payment)
            else:
                payment = self.mollie.payments.get(payment)

        method = self.get_method(payment.id)
        order = self.get_order_with_method(payment.id, method)
        amount = D(payment.get("amount").get("value"))
        msg = "Mollie payment update"

        if self.is_mollie_order(payment):
            status, msg = self.handle_mollie_order_status(
                payment, order, method, amount, msg
            )
        else:
            status, msg = self.handle_mollie_payment_status(
                payment, order, method, amount, msg
            )

        if status is None:
            return

        create_or_record_payment_events(order, status, msg, payment.id)

        self.send_mail_if_criteria_matches(order, method)

    def handle_mollie_payment_status(self, payment, order, method, amount, msg):
        if payment.has_refunds():
            total_refunded = D(payment.amount_refunded.get("value"))
            # Get the most recent refund
            recent_refund = payment.refunds.list()["_embedded"]["refunds"][0]
            refund_status = recent_refund.get("status")
            # Only register the refund once the refund isn't cancelable anymore!
            if refund_status in ["queued", "pending"]:
                logger.info(
                    "Refund can still be canceled. We will skip until we rechieve a definite status"
                )
                return None, None

            status = PIPELINE_STATUS_REFUND
            msg = self.refund_order(payment, method, amount, order, recent_refund)

            # The refund amount, for some payments, can be up to €25.00 more than the original transaction amount.
            # Thats why we check if it is greater or equal to.
            # If it isn't just return and don't record_payment_events
            if not total_refunded >= order.total_incl_tax:
                return None, None
        elif payment.is_paid():
            # Once you cancel a refund, mollie returns a call to the webhook with the exact same order
            # First check that the order last status is actually not equal to paid before setting it.
            status = PIPELINE_STATUS_PAID
            self.complete_order(order, amount, payment.id, status, method)
        elif payment_status(payment) in {"open", "pending", "authorized"}:
            status = PIPELINE_STATUS_PENDING_PAYMENT
        elif payment_status(payment) in {"failed", "canceled", "expired"}:
            status = PIPELINE_STATUS_CANCELLED
        else:
            return None, None

        return status, msg

    def handle_mollie_order_status(self, payment, order, method, amount, msg):
        """Handle a post purchase payment with the Mollie order API"""
        if payment.is_authorized():
            self.complete_order(order, amount, payment.id, PIPELINE_STATUS_PAID, method)
            return PIPELINE_STATUS_PAID, "Create Mollie order"

        return None, msg

    def complete_order(self, order, amount, reference, status, method):
        if not order.status == PIPELINE_STATUS_PAID:
            try:
                source = order.sources.get(
                    source_type=self.get_source(method), reference=reference
                )
                source.debit(amount, reference=reference, status=status)
            except Source.DoesNotExist:
                raise UnableToTakePayment(
                    "Something went wrong. Please contact the administrator."
                )

    def refund_order(self, payment, method, amount, order, recent_refund):
        try:
            reference = payment.id
            refund_status = recent_refund.get("status")
            currently_refunded = D(recent_refund.get("amount").get("value"))
            source = order.sources.get(
                source_type=self.get_source(method), reference=reference
            )
            # Register the refund so you'll see it under transactions
            source.refund(currently_refunded, reference=reference, status=refund_status)
            # Make sure to recalculate the debit
            msg = "%s refunded. %s order total %s has been refunded. Status: %s" % (
                currently_refunded,
                amount,
                D(payment.amount_refunded.get("value")),
                refund_status,
            )
            logger.info(msg)
            return msg
        except Source.DoesNotExist:
            raise UnableToTakePayment(
                "Something went wrong. Please contact the administrator."
            )

    def send_mail_if_criteria_matches(self, order, method):
        if method in [BANKTRANSFER] and order.status == PIPELINE_STATUS_PENDING_PAYMENT:
            send_order_confirmation_message("ORDER_PLACED", order)

    def get_source(self, method):
        code = "mollie:%s" % method
        defaults = {"name": "Mollie %s" % method}
        source_type, __ = SourceType.objects.get_or_create(code=code, defaults=defaults)
        return source_type

    def is_mollie_order(self, payment):
        return True if isinstance(payment, MollieOrder) else False

    def get_method(self, payment_id):
        if payment_id.startswith("ord_"):
            return self.mollie.orders.get(payment_id)["method"]

        return self.mollie.payments.get(payment_id)["method"]
