from PyPDF2 import PdfFileWriter, PdfFileReader, PdfFileMerger
from django.template.loader import render_to_string
from django.views.generic import View, TemplateView
from django.http.response import FileResponse, JsonResponse
from django.db import transaction
from decimal import Decimal as D
from decimal import InvalidOperation
from django.contrib import messages
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Count, Q, Sum, fields
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, UpdateView

from apps.customer.utils import get_supported_countries
from oscar.apps.order import exceptions as order_exceptions
from oscar.apps.payment.exceptions import PaymentError
from oscar.core.loading import get_class, get_model
from oscar.apps.dashboard.orders.views import OrderListView as BaseOrderListView
from django.utils import timezone
from datetime import datetime
import io
import img2pdf
from subscription.utils import create_next_order
from subscription.models import Subscription
from django.conf import settings
from urllib.parse import unquote_plus
import re
from subscription.forms import RanOutOfProductsForm
from django_rq import enqueue
from communication.tasks import send_email
from communication.settings import MANDRILL_TEMPLATES

ShippingEvent = get_model('order', 'ShippingEvent')
Partner = get_model('partner', 'Partner')
Transaction = get_model('payment', 'Transaction')
SourceType = get_model('payment', 'SourceType')
Order = get_model('order', 'Order')
OrderNote = get_model('order', 'OrderNote')
ShippingAddress = get_model('order', 'ShippingAddress')
Line = get_model('order', 'Line')
ShippingEventType = get_model('order', 'ShippingEventType')
PaymentEventType = get_model('order', 'PaymentEventType')
EventHandler = get_class('order.processing', 'EventHandler')
OrderStatsForm = get_class('dashboard.orders.forms', 'OrderStatsForm')
OrderSearchForm = get_class('dashboard.orders.forms', 'OrderSearchForm')
OrderNoteForm = get_class('dashboard.orders.forms', 'OrderNoteForm')
ShippingAddressForm = get_class('dashboard.orders.forms', 'ShippingAddressForm')
OrderStatusForm = get_class('dashboard.orders.forms', 'OrderStatusForm')
ProductClass = get_model('catalogue', 'ProductClass')
Product = get_model('catalogue', 'Product')
ProductAttributeValue = get_model('catalogue', 'ProductAttributeValue')
StockRecord = get_model('partner', 'StockRecord')
Country = get_model('address', 'Country')


class OutOfCoffeeView(TemplateView):
    template_name = 'subscription/partials/out_of_coffee.html'

    def get_context_data(self, **kwargs):
        cd = super(OutOfCoffeeView, self).get_context_data(**kwargs)
        cd['form'] = RanOutOfProductsForm()
        return cd

    def post(self, *args, **kwargs):
        """
        Validate the form and que the emails
        :param args:
        :param kwargs:
        :return:
        """
        form = RanOutOfProductsForm(self.request.POST)
        if form.is_valid():
            product = form.cleaned_data['out_of']
            replacement = form.cleaned_data['replacement']
            users = set()
            # Get all the subscriptions with the product or its siblings and update them.
            subscriptions = Subscription.objects.filter(
                Q(product=product) | Q(product__parent=product)
            ).filter(
                status__in=[Subscription.ACTIVE, Subscription.PAUSED]
            ).prefetch_related('product',
                               'product__parent',
                               'product__attribute_values').distinct().all()

            for subscription in subscriptions:
                users.add(subscription.user)
                subscription_product = subscription.product
                weight, packaging = getattr(subscription_product.attr, 'weight', None), getattr(
                    subscription_product.attr, 'packaging', None)
                # If the product is a pod
                if not weight and not packaging:
                    subscription.product = Product.objects.filter(parent=replacement, subscription=True).first()
                    subscription.save()
                else:
                    # if the product is child, then find the identical cousin from the replacement product
                    # this is hard coded as I found any other method will be too taxing on both mysql server and error pron
                    query = Product.objects.filter(parent=replacement, subscription=True)
                    for child in query.all():
                        if weight == child.attr.weight and packaging == child.attr.packaging:
                            subscription.product = child
                            subscription.save()

            # Get all the future subscription orders with the product or its siblings
            orders = Order.objects.filter(status=settings.ORDER_STATUS_PENDING).filter(
                Q(lines__product=product) | Q(lines__product__parent=product)
            ).prefetch_related('lines', 'lines__product', 'lines__stockrecord').distinct().all()

            for order in orders:
                users.add(order.user)
                # Find and replace the product just like above.
                for line in order.lines.all():

                    p = line.product
                    weight, packaging = getattr(p.attr, 'weight', None), getattr(p.attr, 'packaging', None)
                    if not weight and not packaging:
                        line.product = Product.objects.filter(parent=replacement, subscription=True).first()
                        line.title = replacement.get_title()
                        line.stockrecord = StockRecord.objects.filter(product=product,
                                                                      price_currency=order.currency).first()
                        line.save()
                    else:
                        query = Product.objects.filter(parent=replacement)
                        for child in query.all():
                            if p.attr.weight == child.attr.weight and p.attr.packaging == child.attr.packaging:
                                line.product = child
                                line.stockrecord = StockRecord.objects.filter(product=child,
                                                                              price_currency=order.currency).first()
                                line.title = replacement.get_title()
                                line.save()
            # Queue the notifications for the owners
            for user in users:
                enqueue(send_email,
                        subject='Hope you don\'t mind! We will be sending you something different',
                        template=MANDRILL_TEMPLATES['RAN_OUT_OF_COFFEE'],
                        to_email=user.email,
                        from_email='Hook Coffee Roastery <hola@hookcoffee.com.sg>',
                        merge_vars={
                            'USERNAME': user.first_name if user.first_name else '',
                            'OLD_COFFEE': product.get_title(),
                            'NEW_COFFEE': replacement.get_title()
                        })
            messages.success(self.request, 'Success')
            return redirect('dashboard:order-list')


class LabelPDFView(View):
    """
    Print the product label
    """

    def get(self, *args, **kwargs):
        order_id = self.kwargs.get('order_id')
        order = Order.objects.filter(pk=order_id).prefetch_related('lines', 'lines__product').first()
        for item in order.lines.all():
            # Create all the pdfs and append as new page
            pass


class ProcessShipmentView(View):

    def get(self, *args, **kwargs):
        return redirect('dashbord:order-list')

    def post(self, request, *args, **kwargs):
        qr = self.request.POST.get('order_qr', '')
        url = self.request.POST.get('return_url', '')
        if not qr:
            messages.error(self.request, 'Please enter the order number')
            return redirect(url)
        # get the order number from the qr-code, then prin the shipping address for the order.
        matches = re.findall(r"@(.*?)/", unquote_plus(qr))
        if not len(matches):
            messages.error(self.request, 'Order ID not found')
            return redirect('dashboard:order-list')
        return JsonResponse({
            'status': 'success',
            'url': str(reverse_lazy('dashboard:print-shipping-address', kwargs={'number': matches[0]}))
        })


class ProcessOrderView(View):

    def get(self, *args, **kwargs):
        pk = self.kwargs.get('pk', None)
        if not pk:
            return redirect('dashboard:order-list')
        order = get_object_or_404(Order, pk=pk)
        images = []
        filename = 'product_labels.pdf'
        if order.process_order():
            for line in order.lines.all():
                for quantity in range(0, line.quantity):
                    images.append(line.create_label())
        if images:
            output = io.BytesIO()
            img2pdf.convert(images, outputstream=output)
            response = HttpResponse(content_type='application/pdf')
            response['Content-Disposition'] = 'attachment;filename="{}"'.format(filename)
            response.write(output.getvalue())
            return response
        messages.error(self.request, 'There are no PDFs to print')
        return redirect('dashboard:order-list')


class ShippingAddressView(View):
    """
    Print the shipping address or waybill.
    """

    def get(self, *args, **kwargs):
        number = self.kwargs.get('number').strip()
        filename_template = 'address-{}.pdf'
        order = Order.objects.filter(number=number).prefetch_related('shipping_address').first()
        with transaction.atomic():
            address_pdf = order.create_shipping_label()
            order.status = 'Shipped'
            order.save()
        if address_pdf:
            response = HttpResponse(content_type='application/pdf')
            response['Content-Disposition'] = 'attachment;filename="{}.pdf"'.format(order.number)
            # output.write(response)
            response.write(address_pdf)
            return response
        messages.error(self.request, 'There is no PDF to print yet')
        return redirect('dashboard:order-list')


class ProductLabelView(View):

    def get(self, *args, **kwargs):
        number = self.kwargs.get('number')
        order = Order.objects.filter(number=number).prefetch_related('lines', 'shipping_address',
                                                                     'lines__product').first()
        filename = 'Product-Labels-Form-{}.pdf'.format(order.number)
        label_images = []
        for line in order.lines.all():
            for quantity in range(0, line.quantity):
                label_images.append(line.create_label())

        merger = io.BytesIO()
        img2pdf.convert(label_images, outputstream=merger)

        response = HttpResponse(content_type='application/pdf')
        response['Content-Disposition'] = 'attachment;filename="{}"'.format(filename)
        # output.write(response)
        response.write(merger.getvalue())
        return response


def queryset_orders_for_user(user):
    """
    Returns a queryset of all orders that a user is allowed to access.
    A staff user may access all orders.
    To allow access to an order for a non-staff user, at least one line's
    partner has to have the user in the partner's list.
    """
    queryset = Order._default_manager.select_related(
        'billing_address', 'billing_address__country',
        'shipping_address', 'shipping_address__country',
        'user'
    ).prefetch_related('lines', 'status_changes')
    if user.is_staff:
        return queryset
    else:
        partners = Partner._default_manager.filter(users=user)
        return queryset.filter(lines__partner__in=partners).distinct()


def get_order_for_user_or_404(user, number):
    try:
        return queryset_orders_for_user(user).get(number=number)
    except ObjectDoesNotExist:
        raise Http404()


class OrderListView(BaseOrderListView):

    def get_queryset(self):
        qs = super(OrderListView, self).get_queryset()
        return qs.prefetch_related('lines', 'lines__attributes', 'lines__product', 'lines__product__attribute_values')

    def todays_repeaters(self):
        shipping_date = datetime.strptime(self.request.GET.get('shipping_date'), '%d-%m-%Y').date()
        country = self.request.GET.get('country', 'SG')
        queryset = Order.objects.values('user_id').annotate(user_count=Count('user_id')).filter(
            lines__est_dispatch_date=shipping_date
        ).filter(
            shipping_address__country__pk=country
        ).filter(user_count__gt=1).order_by()
        return [item['user_id'] for item in queryset]

    def sub_queryset(self):
        self.todays_repeaters()
        qs = super(OrderListView, self).get_queryset()
        shipping_date = datetime.strptime(self.request.GET.get('shipping_date'), '%d-%m-%Y').date()
        backlog = True if self.request.GET.get('backlog', 'no') == 'yes' else False
        sub_qs = qs.prefetch_related(
            'lines', 'lines__attributes', 'lines__product', 'lines__product__attribute_values'
        )
        if backlog:
            sub_qs = sub_qs.filter(lines__est_dispatch_date__lte=shipping_date)
        else:
            sub_qs = sub_qs.filter(lines__est_dispatch_date=shipping_date)
        sub_qs = sub_qs.filter(
            shipping_address__country__pk=self.request.GET.get('country', 'SG')
        ).annotate(
            line_count=Count('lines'),
            user_count=Count('user_id')
        ).filter(line_count=1).filter(
            status__in=[settings.ORDER_STATUS_PENDING, settings.ORDER_STATUS_BEING_PROCESSED]
        ).exclude(user__pk__in=self.todays_repeaters())

        return sub_qs

    def dispatch(self, request, *args, **kwargs):
        d = super(OrderListView, self).dispatch(request, *args, **kwargs)
        if request.GET.get('process_shipping', None) == 'true':
            return self.process_shipping()
        if request.GET.get('process_orders', None) == 'true':
            return self.process_orders()
        return d

    def process_orders(self):
        """
        Process all the orders from the query set and print the labels.
        Process the payment for orders that are in the pending status.
        :return:
        """
        images = []
        filename = 'product_labels.pdf'
        for order in self.get_queryset().all():
            if order.process_order():
                for line in order.lines.all():
                    images.append(line.create_label())
        if images:
            output = io.BytesIO()
            img2pdf.convert(images, outputstream=output)

            response = HttpResponse(content_type='application/pdf')
            response['Content-Disposition'] = 'attachment;filename="{}"'.format(filename)
            response.write(output.getvalue())
            return response
        messages.error(self.request, 'There are no PDFs to print')
        return redirect('dashboard:order-list')

    def process_shipping(self):
        """
        Print the shipping labels for all the orders.
        :return:
        """
        with transaction.atomic():
            output = PdfFileWriter()
            filename = 'shipping_labels.pdf'
            for order in self.get_queryset().all():
                order.status = 'Shipped'
                order.save()
                page_buffer = order.create_shipping_label()
                page = PdfFileReader(page_buffer).getPage(0)
                output.addPage(page)
            response = HttpResponse(content_type='application/pdf')
            response['Content-Disposition'] = 'attachment;filename="{}"'.format(filename)
            output.write(response)
            return response

    def get_context_data(self, **kwargs):
        cd = super(OrderListView, self).get_context_data(**kwargs)
        today = timezone.localdate().strftime('%d-%m-%Y')
        shipping_date = self.request.GET.get('shipping_date', today)
        backlog = self.request.GET.get('backlog', 'no')
        cd['backlog'] = backlog
        cd['shipping_date'] = shipping_date
        cd['countries'] = get_supported_countries()
        country_selected = self.request.GET.get('country', 'SG')
        cd['country_selected'] = country_selected
        cd['get_args'] = "?shipping_date={}&country={}&backlog={}".format(shipping_date, country_selected, backlog)
        cd['being_processed'] = settings.ORDER_STATUS_BEING_PROCESSED
        cd['pending'] = settings.ORDER_STATUS_PENDING
        cd['shipped'] = settings.ORDER_STATUS_SHIPPED
        return cd


class CoffeeOrderListView(OrderListView):
    paginate_by = 5000

    def dispatch(self, request, *args, **kwargs):
        if not request.GET.get('shipping_date', None):
            messages.error(request, 'PLease select the shipping date')
            return redirect('dashboard:order-list')
        return super(CoffeeOrderListView, self).dispatch(request, *args, **kwargs)

    def get_queryset(self):
        qs = self.sub_queryset()
        # return qs.count_related(Line).filter(lines__count=1)
        coffee_bags = ProductClass.objects.get(slug='coffee-bag')
        return qs.filter(
            Q(lines__product__product_class=coffee_bags) | Q(lines__product__parent__product_class=coffee_bags)
        ).distinct()

    def get_context_data(self, **kwargs):
        cd = super(CoffeeOrderListView, self).get_context_data(**kwargs)
        cd['active_tab'] = 'coffees'
        return cd


class PodsOrderListView(OrderListView):
    paginate_by = 5000

    def dispatch(self, request, *args, **kwargs):
        if not request.GET.get('shipping_date', None):
            messages.error(request, 'PLease select the shipping date')
            return redirect('dashboard:order-list')
        return super(PodsOrderListView, self).dispatch(request, *args, **kwargs)

    def get_queryset(self):
        coffee_pods = ProductClass.objects.get(slug='coffee-pods')
        qs = self.sub_queryset()
        return qs.filter(
            Q(lines__product__product_class=coffee_pods) | Q(lines__product__parent__product_class=coffee_pods)
        ).distinct()

    def get_context_data(self, **kwargs):
        cd = super(PodsOrderListView, self).get_context_data(**kwargs)
        cd['active_tab'] = 'pods'
        return cd


class HookBagsOrderListView(OrderListView):
    paginate_by = 5000

    def get_queryset(self):
        hook_bags = ProductClass.objects.get(slug='hook-bags')
        qs = self.sub_queryset()
        return qs.filter(
            Q(lines__product__product_class=hook_bags) | Q(lines__product__parent__product_class=hook_bags)
        ).distinct()

    def dispatch(self, request, *args, **kwargs):
        if not request.GET.get('shipping_date', None):
            messages.error(request, 'PLease select the shipping date')
            return redirect('dashboard:order-list')
        return super(HookBagsOrderListView, self).dispatch(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        cd = super(HookBagsOrderListView, self).get_context_data(**kwargs)
        cd['active_tab'] = 'hook-bags'
        return cd


class GearsOrderListView(OrderListView):
    paginate_by = 5000

    def dispatch(self, request, *args, **kwargs):
        if not request.GET.get('shipping_date', None):
            messages.error(request, 'PLease select the shipping date')
            return redirect('dashboard:order-list')
        return super(GearsOrderListView, self).dispatch(request, *args, **kwargs)

    def get_queryset(self):
        essential_gears = ProductClass.objects.get(slug='essential-gears')
        coffee_machines = ProductClass.objects.get(slug='coffee-machines')
        gift_sets = ProductClass.objects.get(slug='gift-sets')
        qs = self.sub_queryset()
        return qs.filter(
            Q(lines__product__product_class=essential_gears) | Q(
                lines__product__parent__product_class=essential_gears) |
            Q(lines__product__product_class=coffee_machines) | Q(
                lines__product__parent__product_class=coffee_machines) |
            Q(lines__product__product_class=gift_sets) | Q(lines__product__parent__product_class=gift_sets)
        ).distinct()

    def get_context_data(self, **kwargs):
        cd = super(GearsOrderListView, self).get_context_data(**kwargs)
        cd['active_tab'] = 'gears'
        return cd


class MultipleOrderListView(OrderListView):
    paginate_by = 5000

    def dispatch(self, request, *args, **kwargs):
        if not request.GET.get('shipping_date', None):
            messages.error(request, 'PLease select the shipping date')
            return redirect('dashboard:order-list')
        return super(MultipleOrderListView, self).dispatch(request, *args, **kwargs)

    def get_queryset(self):
        qs = super(MultipleOrderListView, self).get_queryset()
        shipping_date = datetime.strptime(self.request.GET.get('shipping_date'), '%d-%m-%Y').date()
        return qs.annotate(line_count=Count('lines')).filter(
            Q(line_count__gt=1) | Q(user__pk__in=self.todays_repeaters())
        ).filter(
            lines__est_dispatch_date=shipping_date
        ).filter(
            shipping_address__country__pk=self.request.GET.get('country', 'SG')
        ).filter(
            status__in=[settings.ORDER_STATUS_BEING_PROCESSED, settings.ORDER_STATUS_PENDING]).order_by(
            'user_id').distinct()

    def get_context_data(self, **kwargs):
        cd = super(MultipleOrderListView, self).get_context_data(**kwargs)
        cd['active_tab'] = 'multiple'
        return cd


class DeclinedOrderListView(OrderListView):
    paginate_by = 5000

    def get_queryset(self):
        qs = super(DeclinedOrderListView, self).get_queryset()
        return qs.filter(
            shipping_address__country__pk=self.request.GET.get('country', 'SG')
        ).filter(status=settings.ORDER_STATUS_ERROR)

    def get_context_data(self, **kwargs):
        cd = super(DeclinedOrderListView, self).get_context_data(**kwargs)
        cd['active_tab'] = 'declined'
        return cd


class WorkshopOrderListView(OrderListView):
    paginate_by = 5000

    def dispatch(self, request, *args, **kwargs):
        if not request.GET.get('shipping_date', None):
            messages.error(request, 'Please select the shipping date')
            return redirect('dashboard:order-list')
        return super(WorkshopOrderListView, self).dispatch(request, *args, **kwargs)

    def get_queryset(self):
        workshops = ProductClass.objects.get(slug='workshops')
        qs = self.sub_queryset()
        return qs.filter(
            Q(lines__product__product_class=workshops) | Q(lines__product__parent__product_class=workshops)
        ).distinct()

    def get_context_data(self, **kwargs):
        cd = super(WorkshopOrderListView, self).get_context_data(**kwargs)
        cd['active_tab'] = 'workshops'
        return cd


class OrderDetailView(DetailView):
    """
    Dashboard view to display a single order.

    Supports the permission-based dashboard.
    """
    model = Order
    context_object_name = 'order'
    template_name = 'dashboard/orders/order_detail.html'

    # These strings are method names that are allowed to be called from a
    # submitted form.
    order_actions = ('save_note', 'delete_note', 'change_order_status',
                     'create_order_payment_event')
    line_actions = ('change_line_statuses', 'create_shipping_event',
                    'create_payment_event')

    def get_object(self, queryset=None):
        return get_order_for_user_or_404(
            self.request.user, self.kwargs['number'])

    def get_order_lines(self):
        return self.object.lines.all()

    def post(self, request, *args, **kwargs):
        # For POST requests, we use a dynamic dispatch technique where a
        # parameter specifies what we're trying to do with the form submission.
        # We distinguish between order-level actions and line-level actions.

        order = self.object = self.get_object()

        # Look for order-level action first
        if 'order_action' in request.POST:
            return self.handle_order_action(
                request, order, request.POST['order_action'])

        # Look for line-level action
        if 'line_action' in request.POST:
            return self.handle_line_action(
                request, order, request.POST['line_action'])

        return self.reload_page(error=_("No valid action submitted"))

    def handle_order_action(self, request, order, action):
        if action not in self.order_actions:
            return self.reload_page(error=_("Invalid action"))
        return getattr(self, action)(request, order)

    def handle_line_action(self, request, order, action):
        if action not in self.line_actions:
            return self.reload_page(error=_("Invalid action"))

        # Load requested lines
        line_ids = request.POST.getlist('selected_line')
        if len(line_ids) == 0:
            return self.reload_page(error=_(
                "You must select some lines to act on"))

        lines = self.get_order_lines()
        lines = lines.filter(id__in=line_ids)
        if len(line_ids) != len(lines):
            return self.reload_page(error=_("Invalid lines requested"))

        # Build list of line quantities
        line_quantities = []
        for line in lines:
            qty = request.POST.get('selected_line_qty_%s' % line.id)
            try:
                qty = int(qty)
            except ValueError:
                qty = None
            if qty is None or qty <= 0:
                error_msg = _("The entered quantity for line #%s is not valid")
                return self.reload_page(error=error_msg % line.id)
            elif qty > line.quantity:
                error_msg = _(
                    "The entered quantity for line #%(line_id)s "
                    "should not be higher than %(quantity)s")
                kwargs = {'line_id': line.id, 'quantity': line.quantity}
                return self.reload_page(error=error_msg % kwargs)

            line_quantities.append(qty)

        return getattr(self, action)(
            request, order, lines, line_quantities)

    def reload_page(self, fragment=None, error=None):
        url = reverse('dashboard:order-detail',
                      kwargs={'number': self.object.number})
        if fragment:
            url += '#' + fragment
        if error:
            messages.error(self.request, error)
        return HttpResponseRedirect(url)

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['active_tab'] = kwargs.get('active_tab', 'lines')

        # Forms
        ctx['note_form'] = self.get_order_note_form()
        ctx['order_status_form'] = self.get_order_status_form()

        ctx['lines'] = self.get_order_lines()
        ctx['line_statuses'] = Line.all_statuses()
        ctx['shipping_event_types'] = ShippingEventType.objects.all()
        ctx['payment_event_types'] = PaymentEventType.objects.all()

        ctx['payment_transactions'] = self.get_payment_transactions()

        return ctx

    # Data fetching methods for template context

    def get_payment_transactions(self):
        return Transaction.objects.filter(
            source__order=self.object)

    def get_order_note_form(self):
        kwargs = {
            'order': self.object,
            'user': self.request.user,
            'data': None
        }
        if self.request.method == 'POST':
            kwargs['data'] = self.request.POST
        note_id = self.kwargs.get('note_id', None)
        if note_id:
            note = get_object_or_404(OrderNote, order=self.object, id=note_id)
            if note.is_editable():
                kwargs['instance'] = note
        return OrderNoteForm(**kwargs)

    def get_order_status_form(self):
        data = None
        if self.request.method == 'POST':
            data = self.request.POST
        return OrderStatusForm(order=self.object, data=data)

    # Order-level actions

    def save_note(self, request, order):
        form = self.get_order_note_form()
        if form.is_valid():
            form.save()
            messages.success(self.request, _("Note saved"))
            return self.reload_page(fragment='notes')

        ctx = self.get_context_data(note_form=form, active_tab='notes')
        return self.render_to_response(ctx)

    def delete_note(self, request, order):
        try:
            note = order.notes.get(id=request.POST.get('note_id', None))
        except ObjectDoesNotExist:
            messages.error(request, _("Note cannot be deleted"))
        else:
            messages.info(request, _("Note deleted"))
            note.delete()
        return self.reload_page()

    def change_order_status(self, request, order):
        form = self.get_order_status_form()
        if not form.is_valid():
            return self.reload_page(error=_("Invalid form submission"))

        old_status, new_status = order.status, form.cleaned_data['new_status']
        handler = EventHandler(request.user)

        success_msg = _(
            "Order status changed from '%(old_status)s' to "
            "'%(new_status)s'") % {'old_status': old_status,
                                   'new_status': new_status}
        try:
            handler.handle_order_status_change(
                order, new_status, note_msg=success_msg)
        except PaymentError as e:
            messages.error(
                request, _("Unable to change order status due to "
                           "payment error: %s") % e)
        except order_exceptions.InvalidOrderStatus as e:
            # The form should validate against this, so we should only end up
            # here during race conditions.
            messages.error(
                request, _("Unable to change order status as the requested "
                           "new status is not valid"))
        else:
            messages.info(request, success_msg)
        return self.reload_page()

    def create_order_payment_event(self, request, order):
        """
        Create a payment event for the whole order
        """
        amount_str = request.POST.get('amount', None)
        try:
            amount = D(amount_str)
        except InvalidOperation:
            messages.error(request, _("Please choose a valid amount"))
            return self.reload_page()
        return self._create_payment_event(request, order, amount)

    # Line-level actions

    def change_line_statuses(self, request, order, lines, quantities):
        new_status = request.POST['new_status'].strip()
        if not new_status:
            messages.error(request, _("The new status '%s' is not valid")
                           % new_status)
            return self.reload_page()
        errors = []
        for line in lines:
            if new_status not in line.available_statuses():
                errors.append(_("'%(status)s' is not a valid new status for"
                                " line %(line_id)d") % {'status': new_status,
                                                        'line_id': line.id})
        if errors:
            messages.error(request, "\n".join(errors))
            return self.reload_page()

        msgs = []
        for line in lines:
            msg = _("Status of line #%(line_id)d changed from '%(old_status)s'"
                    " to '%(new_status)s'") % {'line_id': line.id,
                                               'old_status': line.status,
                                               'new_status': new_status}
            msgs.append(msg)
            line.set_status(new_status)
        message = "\n".join(msgs)
        messages.info(request, message)
        order.notes.create(user=request.user, message=message,
                           note_type=OrderNote.SYSTEM)
        return self.reload_page()

    def create_shipping_event(self, request, order, lines, quantities):
        code = request.POST['shipping_event_type']
        try:
            event_type = ShippingEventType._default_manager.get(code=code)
        except ShippingEventType.DoesNotExist:
            messages.error(request, _("The event type '%s' is not valid")
                           % code)
            return self.reload_page()

        reference = request.POST.get('reference', None)
        try:
            EventHandler().handle_shipping_event(order, event_type, lines,
                                                 quantities,
                                                 reference=reference)
        except order_exceptions.InvalidShippingEvent as e:
            messages.error(request,
                           _("Unable to create shipping event: %s") % e)
        except order_exceptions.InvalidStatus as e:
            messages.error(request,
                           _("Unable to create shipping event: %s") % e)
        except PaymentError as e:
            messages.error(request, _("Unable to create shipping event due to"
                                      " payment error: %s") % e)
        else:
            messages.success(request, _("Shipping event created"))
        return self.reload_page()

    def create_payment_event(self, request, order, lines, quantities):
        """
        Create a payment event for a subset of order lines
        """
        amount_str = request.POST.get('amount', None)

        # If no amount passed, then we add up the total of the selected lines
        if not amount_str:
            amount = sum([line.line_price_incl_tax for line in lines])
        else:
            try:
                amount = D(amount_str)
            except InvalidOperation:
                messages.error(request, _("Please choose a valid amount"))
                return self.reload_page()

        return self._create_payment_event(request, order, amount, lines,
                                          quantities)

    def _create_payment_event(self, request, order, amount, lines=None,
                              quantities=None):
        code = request.POST.get('payment_event_type')
        try:
            event_type = PaymentEventType._default_manager.get(code=code)
        except PaymentEventType.DoesNotExist:
            messages.error(
                request, _("The event type '%s' is not valid") % code)
            return self.reload_page()
        try:
            EventHandler().handle_payment_event(
                order, event_type, amount, lines, quantities)
        except PaymentError as e:
            messages.error(request, _("Unable to create payment event due to"
                                      " payment error: %s") % e)
        except order_exceptions.InvalidPaymentEvent as e:
            messages.error(
                request, _("Unable to create payment event: %s") % e)
        else:
            messages.info(request, _("Payment event created"))
        return self.reload_page()


class LineDetailView(DetailView):
    """
    Dashboard view to show a single line of an order.
    Supports the permission-based dashboard.
    """
    model = Line
    context_object_name = 'line'
    template_name = 'dashboard/orders/line_detail.html'

    def get_object(self, queryset=None):
        order = get_order_for_user_or_404(self.request.user,
                                          self.kwargs['number'])
        try:
            return order.lines.get(pk=self.kwargs['line_id'])
        except self.model.DoesNotExist:
            raise Http404()

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['order'] = self.object.order
        return ctx


def get_changes_between_models(model1, model2, excludes=None):
    """
    Return a dict of differences between two model instances
    """
    if excludes is None:
        excludes = []
    changes = {}
    for field in model1._meta.fields:
        if (isinstance(field, (fields.AutoField,
                               fields.related.RelatedField))
                or field.name in excludes):
            continue

        if field.value_from_object(model1) != field.value_from_object(model2):
            changes[field.verbose_name] = (field.value_from_object(model1),
                                           field.value_from_object(model2))
    return changes


def get_change_summary(model1, model2):
    """
    Generate a summary of the changes between two address models
    """
    changes = get_changes_between_models(model1, model2, ['search_text'])
    change_descriptions = []
    for field, delta in changes.items():
        change_descriptions.append(_("%(field)s changed from '%(old_value)s'"
                                     " to '%(new_value)s'")
                                   % {'field': field,
                                      'old_value': delta[0],
                                      'new_value': delta[1]})
    return "\n".join(change_descriptions)


class ShippingAddressUpdateView(UpdateView):
    """
    Dashboard view to update an order's shipping address.
    Supports the permission-based dashboard.
    """
    model = ShippingAddress
    context_object_name = 'address'
    template_name = 'dashboard/orders/shippingaddress_form.html'
    form_class = ShippingAddressForm

    def get_object(self, queryset=None):
        order = get_order_for_user_or_404(self.request.user,
                                          self.kwargs['number'])
        return get_object_or_404(self.model, order=order)

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['order'] = self.object.order
        return ctx

    def form_valid(self, form):
        old_address = ShippingAddress.objects.get(id=self.object.id)
        response = super().form_valid(form)
        changes = get_change_summary(old_address, self.object)
        if changes:
            msg = _("Delivery address updated:\n%s") % changes
            self.object.order.notes.create(user=self.request.user, message=msg,
                                           note_type=OrderNote.SYSTEM)
        return response

    def get_success_url(self):
        messages.info(self.request, _("Delivery address updated"))
        return reverse('dashboard:order-detail',
                       kwargs={'number': self.object.order.number, })


class ProcessOrdersListView(OrderListView):
    pass
