# pylint: disable=unused-import
import logging

from django.db import transaction
from django.utils.translation import gettext as _

from oscar.apps.order import exceptions as order_exceptions
from oscar.core.loading import get_class, get_classes, get_model

from ocyan.core.fender import config
from ocyan.main.loading import get_baseclass
from ocyan.plugin.oscar.info import OSCAR_NEEDS_OXYAN
from ocyan.plugin.oscar.pipeline import (
    PIPELINE_STATUS_CANCELLED,
    PIPELINE_STATUS_COMPLETE,
    PIPELINE_STATUS_DELAYED_PAYMENT,
    PIPELINE_STATUS_NEW,
    PIPELINE_STATUS_PAID,
    PIPELINE_STATUS_PENDING_PAYMENT,
    PIPELINE_STATUS_REFUND,
    PIPELINE_STATUS_RESTOCKED,
)
from ocyan.plugin.oscar_shipping.plugins import get_shipping_event_types
from ocyan.plugin.oscar_shipping.shipping.constants import SHIPPING_EVENT_SHIPPED

BaseEventHandler = get_baseclass(
    "order.processing",
    "EventHandler",
    "ocyan.plugin.oscar_communications.order_extension",
)
send_message = get_class("order.shortcuts", "send_message")
send_order_confirmation_message, send_new_order_status_message = get_classes(
    "order.communications",
    ["send_order_confirmation_message", "send_new_order_status_message"],
)
ShippingContext = get_class("order.context", "ShippingContext")
ShippingEventQuantity = get_model("order", "ShippingEventQuantity")
Tracking = get_model("order", "Tracking")

if OSCAR_NEEDS_OXYAN:
    CommunicationEventType = get_model("communication", "CommunicationEventType")
else:
    CommunicationEventType = get_model("customer", "CommunicationEventType")

logger = logging.getLogger(__name__)


class EventHandler(BaseEventHandler):
    def handle_order_status_change(self, order, new_status, note_msg=None):
        with transaction.atomic():
            super(EventHandler, self).handle_order_status_change(
                order, new_status, note_msg
            )
            if new_status in [PIPELINE_STATUS_PAID, PIPELINE_STATUS_DELAYED_PAYMENT]:
                send_order_confirmation_message("ORDER_PLACED", order)
            elif new_status == PIPELINE_STATUS_CANCELLED:
                self.cancel_stock_allocations(order)

            # Search for a communication event specific for the new order status
            status_event_code = "ORDER_STATUS_%s" % new_status.upper()
            status_event_exists = CommunicationEventType.objects.filter(
                code=status_event_code
            ).exists()
            if status_event_exists:
                send_new_order_status_message(status_event_code, order)

    # pylint: disable=unused-argument
    def can_consume_stock_allocations(self, order, event_type, lines, line_quantities):
        shipping_event_types = get_shipping_event_types()

        if event_type.code not in shipping_event_types:
            # we only consume allocations for shipping events that are registered with
            # contribute_to_shipping_event_types
            return False

        shipping_events = order.shipping_events.filter(
            event_type__code__in=shipping_event_types
        )
        if not shipping_events.exists():
            # there has been no allocation at all yet, because no shipping events
            # that trigger allocations have been registered yet
            return True
        else:
            # there allready are shipping events.
            # we use order._is_event_complete
            # to check if the quantities for all shipping event types of type
            # SHIPPING_EVENT_TYPES sum to the total order quantities.
            return not order._is_event_complete(  # pylint: disable=protected-access
                ShippingEventQuantity.objects.filter(
                    event__event_type__code__in=shipping_event_types
                )
            )

    def handle_shipping_event(
        self, order, event_type, lines, line_quantities, **kwargs
    ):
        # consuming stock and shipping event has to be tied together to in a
        # transaction to avoid double allocations in case of errors.
        with transaction.atomic():
            do_consume_stock_allocations = self.can_consume_stock_allocations(
                order, event_type, lines, line_quantities
            )

            # check values and create shipping event
            event = super(EventHandler, self).handle_shipping_event(
                order, event_type, lines, line_quantities, **kwargs
            )

            if do_consume_stock_allocations:
                # seems everything is valid, so consume stock because we took the
                # product and stuck it in a package.
                self.consume_stock_allocations(order, lines, line_quantities)

        if event_type.code in get_shipping_event_types():
            # put all fully shipped lines to status completed
            for line in lines:
                if self.have_lines_passed_shipping_event(
                    order, [line], [line.quantity], event_type
                ):
                    try:
                        line.set_status(PIPELINE_STATUS_COMPLETE)
                    except order_exceptions.InvalidLineStatus:
                        pass  # shut this up, ne need to display error

            # send shipping email if needed.
            if config.get("oscar_communications", "send_shipping_email", False):
                ctx = ShippingContext(event)
                send_message(ctx, SHIPPING_EVENT_SHIPPED)

            # if there are nomore lines that are uncompleted, complete order.
            if not order.lines.exclude(status=PIPELINE_STATUS_COMPLETE).exists():
                try:
                    msg = _("All lines have shipped")
                    self.handle_order_status_change(
                        order, PIPELINE_STATUS_COMPLETE, msg
                    )
                except order_exceptions.InvalidOrderStatus:
                    pass  # shut this up, ne need to display error

        return event

    def create_payment_event(
        self, order, event_type, amount, lines=None, line_quantities=None, **kwargs
    ):
        payment_event = super().create_payment_event(
            order, event_type, amount, lines, line_quantities, **kwargs
        )
        if payment_event.event_type.code == PIPELINE_STATUS_REFUND:
            # update the balance and refund the specified amount
            source = order.sources.first()
            if source is not None:
                source.refund(payment_event.amount)

            # If lines have been fully refunded update the line status to refund
            for line in payment_event.lines.all():
                if line.payment_event_quantity(event_type) == line.quantity:
                    line.set_status(PIPELINE_STATUS_REFUND)

    def cancel_stock_allocations(self, order, lines=None, line_quantities=None):
        if not lines:
            lines = order.lines.exclude(status=PIPELINE_STATUS_RESTOCKED)
        if not line_quantities:
            line_quantities = [line.quantity for line in lines]

        # everything has been allocated zo don't do anything
        if not line_quantities:
            return

        super().cancel_stock_allocations(order, lines, line_quantities)
        for line in lines:
            line.set_status(PIPELINE_STATUS_RESTOCKED)

    def validate_shipping_event(
        self, order, event_type, lines, line_quantities, **kwargs
    ):
        if self.are_stock_allocations_available(lines, line_quantities):
            return super(EventHandler, self).validate_shipping_event(
                order, event_type, lines, line_quantities, **kwargs
            )
        else:
            msg = _(
                "Not enough stock or allocations available, please update stockrecords first."
            )
            raise order_exceptions.InvalidShippingEvent(msg)

    def create_shipping_event(
        self, order, event_type, lines, line_quantities, **kwargs
    ):
        shipping_event = super().create_shipping_event(
            order, event_type, lines, line_quantities, **kwargs
        )
        self.create_tracking(shipping_event, **kwargs)
        return shipping_event

    def create_tracking(
        self,
        shipping_event,
        carrier=None,
        tracking_code=None,
        tracking_url=None,
        **kwargs,
    ):
        """
        Requires carrier and tracking_code or tracking_url in kwargs for it to work.
        """
        if carrier:
            return Tracking.create_for_shipping_event(
                shipping_event, carrier, tracking_code, tracking_url, **kwargs
            )
