# Copyright (c) 2015 Presslabs SRL
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import absolute_import, unicode_literals

import errno
import logging
import os
import requests
from six.moves import map
from collections import OrderedDict

from PyPDF2 import PdfFileReader, PdfFileMerger
from dal import autocomplete
from django_fsm import TransitionNotAllowed

from django import forms
from django.contrib import messages
from django.contrib.admin import (
    helpers, site, TabularInline, ModelAdmin, SimpleListFilter
)
from django.contrib.admin.actions import delete_selected as delete_selected_
from django.contrib.admin.models import LogEntry, CHANGE
from django.contrib.contenttypes.models import ContentType
from django.core import urlresolvers
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.db import connections
from django.db.models import BLANK_CHOICE_DASH
from django.forms import ChoiceField
from django.http import HttpResponse
from django.shortcuts import render
from django.utils import timezone
from django.utils.encoding import force_text
from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _

from silver.documents_generator import DocumentsGenerator
from silver.models import (
    Plan, MeteredFeature, Subscription, Customer, Provider,
    MeteredFeatureUnitsLog, Invoice, DocumentEntry,
    ProductCode, Proforma, BillingLog, BillingDocumentBase,
    Transaction, PaymentMethod
)
from silver.payment_processors.mixins import PaymentProcessorTypes
from silver.utils.international import currencies
from silver.utils.payments import get_payment_url


logger = logging.getLogger('silver')


def metadata(obj):
    d = u'(None)'
    if obj.meta:
        d = u''
        for key, value in obj.meta.items():
            d += u'%s: <code>%s</code><br>' % (escape(key), escape(value))
    return d
metadata.allow_tags = True


def tax(obj):
    return ("{} {:.2f}%".format(obj.sales_tax_name, obj.sales_tax_percent)
            if obj.sales_tax_percent else '')
tax.admin_order_field = 'sales_tax_percent'


class LiveModelAdmin(ModelAdmin):
    def get_queryset(self, request):
        qs = self.model.all_objects.get_queryset()
        ordering = self.get_ordering(request)
        if ordering:
            qs = qs.order_by(*ordering)
        return qs

    def delete_model(self, request, obj):
        if 'hard_delete' in dir(obj):
            obj.hard_delete()
        else:
            super(LiveModelAdmin, self).delete_model(request, obj)

    def delete_selected(self, request, queryset):
        if 'hard_delete' in dir(queryset):
            queryset.delete = queryset.hard_delete
        return delete_selected_(self, request, queryset)

    delete_selected.short_description = _("Delete selected "
                                          "%(verbose_name_plural)s")

    actions = ['delete_selected']


class PlanForm(forms.ModelForm):
    class Meta:
        model = Plan
        fields = ('provider', 'name', 'product_code', 'interval',
                  'interval_count', 'amount', 'currency', 'trial_period_days',
                  'generate_documents_on_trial_end', 'separate_cycles_during_trial', 'prebill_plan',
                  'cycle_billing_duration', 'generate_after', 'metered_features', 'enabled',
                  'private')

    def clean(self):
        metered_features = self.cleaned_data.get('metered_features')
        Plan.validate_metered_features(metered_features)
        return self.cleaned_data


class PlanAdmin(ModelAdmin):
    list_display = ['name', 'description', 'interval_display',
                    'trial_period_days', 'enabled', 'private']
    search_fields = ['name']
    list_filter = ['provider']
    form = PlanForm

    def interval_display(self, obj):
        return ('{:d} {}s'.format(obj.interval_count, obj.interval)
                if obj.interval_count != 1
                else '{:d} {}'.format(obj.interval_count, obj.interval))
    interval_display.short_description = 'Interval'

    def description(self, obj):
        d = u'Subscription: <code>{:.2f} {}</code><br>'.format(obj.amount,
                                                               obj.currency)
        fmt = u'{name}: <code>{price:.2f} {currency}</code>'
        for f in obj.metered_features.all():
            d += fmt.format(
                name=f.name,
                price=f.price_per_unit,
                currency=obj.currency,
            )
            if f.included_units > 0:
                d += u'<code> ({:.2f} included)</code>'.format(f.included_units)
            d += u'<br>'
        return d
    description.allow_tags = True


class MeteredFeatureUnitsLogInLine(TabularInline):
    model = MeteredFeatureUnitsLog
    list_display = ['metered_feature']
    readonly_fields = ('start_date', 'end_date', )
    extra = 0

    def get_formset(self, request, obj=None, **kwargs):
        self.parent_obj = obj
        return super(MeteredFeatureUnitsLogInLine, self).get_formset(
            request, obj, **kwargs)

    def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
        if db_field.name == 'metered_feature' and hasattr(self, 'parent_obj'):
            if self.parent_obj:
                kwargs['queryset'] = db_field.rel.to.objects.filter(**{
                    'plan': self.parent_obj.plan
                })
        return super(MeteredFeatureUnitsLogInLine,
                     self).formfield_for_foreignkey(db_field, request,
                                                    **kwargs)


class BillingLogInLine(TabularInline):
    model = BillingLog
    fields = ['billing_date', 'plan_billed_up_to', 'metered_features_billed_up_to',
              'created_at', 'proforma_link', 'invoice_link']
    readonly_fields = fields
    verbose_name = 'Automatic billing log'
    verbose_name_plural = verbose_name

    def has_add_permission(self, request):
        return False

    def has_delete_permission(self, request, obj=None):
        return False

    def invoice_link(self, obj):
        return obj.invoice.admin_change_url if obj.invoice else 'None'
    invoice_link.short_description = 'Invoice'
    invoice_link.allow_tags = True

    def proforma_link(self, obj):
        return obj.proforma.admin_change_url if obj.proforma else 'None'
    proforma_link.short_description = 'Proforma'
    proforma_link.allow_tags = True


class SubscriptionAdmin(ModelAdmin):
    list_display = ['customer', 'plan', 'last_billing_date', 'trial_end',
                    'start_date', 'ended_at', 'state', metadata]
    list_filter = ['plan', 'state', 'plan__provider', 'customer']
    readonly_fields = ['state', ]
    actions = ['activate', 'cancel_now', 'cancel_at_end_of_cycle', 'end']
    search_fields = ['customer__first_name', 'customer__last_name',
                     'customer__company', 'plan__name', 'meta']
    inlines = [MeteredFeatureUnitsLogInLine, BillingLogInLine]

    def get_queryset(self, request):
        return super(SubscriptionAdmin, self).get_queryset(request) \
                                             .prefetch_related('billing_logs')

    def perform_action(self, request, action, queryset):
        try:
            method = getattr(Subscription, action)
        except AttributeError:
            self.message_user(request, 'Illegal action.', level=messages.ERROR)
            return

        failed_count = 0
        queryset_count = queryset.count()
        for entry in queryset:
            try:
                method(entry)
                entry.save()

                LogEntry.objects.log_action(
                    user_id=request.user.id,
                    content_type_id=ContentType.objects.get_for_model(entry).pk,
                    object_id=entry.id,
                    object_repr=force_text(entry),
                    action_flag=CHANGE,
                    change_message='{action} action initiated by user.'.format(
                        action=action.replace('_', ' ').strip().capitalize()
                    )
                )
            except TransitionNotAllowed:
                failed_count += 1
        if failed_count:
            if failed_count == queryset_count:
                self.message_user(request, 'Illegal state change attempt.',
                                  level=messages.ERROR)
            else:
                self.message_user(request, '%d state(s) changed (%d failed).' %
                                  (queryset_count - failed_count, failed_count),
                                  level=messages.WARNING)
        else:
            self.message_user(request, 'Successfully changed %d state(s).' %
                              queryset_count)

    def activate(self, request, queryset):
        self.perform_action(request, 'activate', queryset)
    activate.short_description = 'Activate the selected Subscription(s) '

    def reactivate(self, request, queryset):
        # NOTE: deactivated for now
        self.perform_action(request, 'reactivate', queryset)
    reactivate.short_description = 'Reactivate the selected Subscription(s) '

    def cancel_now(self, request, queryset):
        self.perform_action(request, '_cancel_now', queryset)
    cancel_now.short_description = 'Cancel the selected Subscription(s) now'

    def cancel_at_end_of_cycle(self, request, queryset):
        self.perform_action(request, '_cancel_at_end_of_billing_cycle', queryset)
    cancel_at_end_of_cycle.short_description = 'Cancel the '\
        'selected Subscription(s) at the end '\
        'of the billing cycle'

    def end(self, request, queryset):
        self.perform_action(request, 'end', queryset)
    end.short_description = 'End the selected Subscription(s) '


class CustomerAdmin(LiveModelAdmin):
    fields = ['company', 'first_name', 'last_name', 'customer_reference',
              'email', 'phone', 'address_1', 'address_2', 'city', 'state',
              'zip_code', 'country', 'currency', 'consolidated_billing',
              'payment_due_days', 'sales_tax_name', 'sales_tax_percent',
              'sales_tax_number', 'extra', 'meta']
    list_display = ['name', 'customer_reference',
                    tax, 'consolidated_billing', metadata]
    search_fields = ['customer_reference', 'first_name', 'last_name', 'company',
                     'address_1', 'address_2', 'city', 'zip_code', 'country',
                     'state', 'email', 'meta']
    actions = ['generate_all_documents']
    exclude = ['live']

    def generate_all_documents(self, request, queryset):
        if request.POST.get('post'):
            billing_date = timezone.now().date()
            DocumentsGenerator().generate(billing_date=billing_date,
                                          customers=queryset,
                                          force_generate=True)

            msg = 'Successfully generated all user{term} documents.'
            if queryset.count() > 1:
                msg = msg.format(term='s\'')
            else:
                msg = msg.format(term='\'s')
            self.message_user(request, msg)

            return None

        active_subs = []
        canceled_subs = []
        for customer in queryset:
            subs = customer.subscriptions.all()
            for sub in subs:
                if sub.state == Subscription.STATES.ACTIVE:
                    active_subs.append(sub)
                elif sub.state == Subscription.STATES.CANCELED:
                    canceled_subs.append(sub)

        if len(active_subs) + len(canceled_subs) == 0:
            msg = 'The user does not have any active or canceled documents.'
            self.message_user(request, msg, level=messages.WARNING)
            return None

        context = {
            'title': _('Are you sure?'),
            'active_subscriptions': active_subs,
            'canceled_subscriptions': canceled_subs,
            'queryset': queryset,
            'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
            'opts': self.model._meta

        }
        return render(request, 'admin/issue_all_customer_documents.html', context)
    generate_all_documents.short_description = 'Generate all user\'s Invoices and Proformas'


class ProviderAdmin(LiveModelAdmin):
    fields = ['company', 'name', 'email', 'address_1', 'address_2', 'city',
              'state', 'zip_code', 'country', 'flow', 'invoice_series',
              'invoice_starting_number', 'proforma_series',
              'proforma_starting_number', 'default_document_state',
              'generate_documents_on_trial_end', 'separate_cycles_during_trial', 'prebill_plan',
              'cycle_billing_duration', 'extra', 'meta']
    list_display = ['name', 'invoice_series_list_display',
                    'proforma_series_list_display', metadata]
    search_fields = ['customer_reference', 'name', 'company', 'address_1',
                     'address_2', 'city', 'zip_code', 'country', 'state',
                     'email', 'meta']
    actions = ['generate_monthly_totals']
    exclude = ['live']

    def invoice_series_list_display(self, obj):
        return '{}-{}'.format(obj.invoice_series, obj.invoice_starting_number)
    invoice_series_list_display.short_description = 'Invoice series starting number'

    def proforma_series_list_display(self, obj):
        return '{}-{}'.format(obj.proforma_series,
                              obj.proforma_starting_number)
    proforma_series_list_display.short_description = 'Proforma series starting number'

    def _compute_monthly_totals(self, model_klass, provider, documents):
        klass_name_plural = model_klass.__name__ + 's'

        totals = {}
        totals[klass_name_plural] = OrderedDict()

        all_documents = documents.filter(provider=provider)
        paid_documents = documents.filter(
            provider=provider,
            state=BillingDocumentBase.STATES.PAID
        )
        documents_months = documents.order_by().filter(
            provider=provider
        ).values(
            'month'
        ).distinct()

        total_draft = sum(
            doc.total for doc in (
                documents.filter(
                    provider=provider,
                    state=BillingDocumentBase.STATES.DRAFT
                )
            )
        )

        totals[klass_name_plural]['draft_total'] = str(total_draft)
        totals[klass_name_plural]['entries'] = OrderedDict()
        documents_months = sorted([month['month']
                                   for month in documents_months
                                   if month['month']])
        for month_value in documents_months:
            if month_value is None:
                continue
            display_date = month_value.strftime('%B %Y')
            totals[klass_name_plural]['entries'][display_date] = OrderedDict()

            all_documents_from_month = all_documents.filter(
                issue_date__month=month_value.month,
                issue_date__year=month_value.year
            )
            paid_documents_from_month = paid_documents.filter(
                issue_date__month=month_value.month,
                issue_date__year=month_value.year
            )
            total = sum(invoice.total for invoice in all_documents_from_month)
            total_paid = sum(invoice.total for invoice in paid_documents_from_month)
            totals[klass_name_plural]['entries'][display_date]['total'] = str(total)
            totals[klass_name_plural]['entries'][display_date]['paid'] = str(total_paid)
            totals[klass_name_plural]['entries'][display_date]['unpaid'] = str(total - total_paid)

        return totals

    def generate_monthly_totals(self, request, queryset):
        totals = {}

        invoices = Invoice.objects.extra(
            select={
                'month': connections[Invoice.objects.db].ops.date_trunc_sql(
                    'month', 'issue_date'
                )
            }
        ).filter(
            provider__in=queryset,
            state__in=[BillingDocumentBase.STATES.DRAFT,
                       BillingDocumentBase.STATES.ISSUED,
                       BillingDocumentBase.STATES.PAID]
        )

        proformas = Proforma.objects.extra(
            select={
                'month': connections[Invoice.objects.db].ops.date_trunc_sql(
                    'month', 'issue_date'
                )
            }
        ).filter(
            provider__in=queryset,
            state__in=[BillingDocumentBase.STATES.DRAFT,
                       BillingDocumentBase.STATES.ISSUED,
                       BillingDocumentBase.STATES.PAID]
        )

        for provider in queryset:
            totals[provider.name] = OrderedDict()
            invoices_total = self._compute_monthly_totals(Invoice, provider,
                                                          invoices)
            totals[provider.name].update(invoices_total)

            proformas_total = self._compute_monthly_totals(Proforma, provider,
                                                           proformas)
            totals[provider.name].update(proformas_total)

        context = {
            'title': _('Monthly totals'),
            'totals': totals,
            'queryset': queryset,
            'opts': self.model._meta
        }

        return render(request, 'admin/monthly_totals.html', context)

    generate_monthly_totals.short_description = 'Generate monthly totals'


class DocumentEntryForm(forms.ModelForm):

    class Meta:
        model = DocumentEntry
        fields = ('description', 'prorated', 'product_code', 'unit',
                  'unit_price', 'quantity', 'start_date', 'end_date')
        widgets = {
            'description': forms.Textarea(attrs={'cols': 50, 'rows': 3})
        }


class DocumentEntryInline(TabularInline):
    model = DocumentEntry
    form = DocumentEntryForm
    extra = 0


class BillingDocumentForm(forms.ModelForm):
    transaction_currency = ChoiceField(
        choices=(BLANK_CHOICE_DASH + list(currencies)), required=False,
    )

    def __init__(self, *args, **kwargs):
        # If it's an edit action, save the provider and the number. Check the
        # save() method to see their usefulness.
        instance = kwargs.get('instance')
        self.initial_number = instance.number if instance else None
        self.initial_series = instance.series if instance else None
        self.provider = instance.provider if instance else None

        super(BillingDocumentForm, self).__init__(*args, **kwargs)

    def clean(self, *args, **kwargs):
        cleaned_data = super(BillingDocumentForm, self).clean(*args, **kwargs)

        customer = cleaned_data.get('customer', None)
        if not customer:
            return cleaned_data

        currency = cleaned_data['currency']

        cleaned_data['transaction_currency'] = (
            cleaned_data['transaction_currency'] or customer.currency or currency
        )

        if self.instance:
            self.instance.transaction_currency = cleaned_data['transaction_currency']

        return cleaned_data

    def save(self, commit=True, *args, **kwargs):
        obj = super(BillingDocumentForm, self).save(commit=False)
        # The provider has changed => generate a new number which corresponds
        # to new provider's count
        if self.provider != obj.provider:
            obj.number = None
        else:
            # If the number input box was just cleaned => place back the
            # old number. This will prevent from having unused numbers.
            if self.initial_number and not obj.number:
                if (obj.series and self.initial_series and
                        obj.series == self.initial_series):
                    obj.number = self.initial_number

        if commit:
            obj.save()
        return obj


class InvoiceForm(BillingDocumentForm):
    class Meta:
        model = Invoice
        # NOTE: The exact fields fill be added in the InvoiceAdmin. This was
        # added here to remove the deprecation warning.
        fields = ()


class ProformaForm(BillingDocumentForm):
    class Meta:
        model = Proforma
        # NOTE: The exact fields fill be added in the ProformaAdmin. This was
        # added here to remove the deprecation warning.
        fields = ()


class DueDateFilter(SimpleListFilter):
    # Human-readable title which will be displayed in the
    # right admin sidebar just above the filter options.
    title = 'due date'

    # Parameter for the filter that will be used in the URL query.
    parameter_name = 'due_date_filter'

    def lookups(self, request, model_admin):
        """
        Returns a list of tuples. The first element in each
        tuple is the coded value for the option that will
        appear in the URL query. The second element is the
        human-readable name for the option that will appear
        in the right sidebar.
        """
        return (
            ('due_this_month', _('All due this month')),
            ('due_today', _('All due today')),
            ('overdue_since_last_month', _('All overdue since last month')),
            ('overdue', _('All overdue'))
        )

    def queryset(self, request, queryset):
        """
        Returns the filtered queryset based on the value
        provided in the query string and retrievable via
        `self.value()`.
        """
        # Compare the requested value (either '80s' or '90s')
        # to decide how to filter the queryset.
        if self.value() == 'due_this_month':
            return queryset.due_this_month()
        if self.value() == 'due_today':
            return queryset.due_today()
        if self.value() == 'overdue_since_last_month':
            return queryset.overdue_since_last_month()
        if self.value() == 'overdue':
            return queryset.overdue()

        return queryset


class BillingDocumentAdmin(ModelAdmin):
    list_display = ['series_number', 'customer', 'state',
                    'provider', 'issue_date', 'due_date', 'paid_date',
                    'cancel_date', tax, 'total', 'transaction_total', 'get_related_document']

    list_filter = ('provider__company', 'state', 'customer', DueDateFilter)

    common_fields = ['company', 'address_1', 'address_2', 'city',
                     'country', 'zip_code', 'state', 'email']
    customer_search_fields = ['customer__{field}'.format(field=field)
                              for field in common_fields + ['first_name',
                                                            'last_name']]
    provider_search_fields = ['provider__{field}'.format(field=field)
                              for field in common_fields + ['name']]
    search_fields = (customer_search_fields + provider_search_fields +
                     ['series', 'number'])

    date_hierarchy = 'issue_date'

    fields = (('series', 'number'), 'provider', 'customer', 'issue_date',
              'due_date', 'paid_date', 'cancel_date',
              ('sales_tax_name', 'sales_tax_percent'), 'currency',
              ('transaction_currency', 'transaction_xe_rate'),
              'transaction_xe_date', 'state', 'total', 'related_document')
    readonly_fields = ('state', 'total', 'related_document')
    inlines = [DocumentEntryInline]
    actions = ['issue', 'pay', 'cancel', 'clone', 'download_selected_documents',
               'mark_pdf_for_generation']

    def get_queryset(self, request):
        return super(BillingDocumentAdmin, self).get_queryset(request) \
                                                .select_related('related_document',
                                                                'customer',
                                                                'provider',
                                                                'pdf')

    @property
    def _model(self):
        raise NotImplementedError

    @property
    def _model_name(self):
        raise NotImplementedError

    def perform_action(self, request, queryset, action):
        method = getattr(self._model, action, None)
        if not method:
            self.message_user(request, 'Illegal action.', level=messages.ERROR)
            return

        failed_changes = []
        failed_actions = []

        readable_action = action.replace('_', ' ').strip().capitalize()

        results = []
        for entry in queryset:
            try:
                result = method(entry)
                if result:
                    results.append(result)
                entry.save()

                LogEntry.objects.log_action(
                    user_id=request.user.id,
                    content_type_id=ContentType.objects.get_for_model(entry).pk,
                    object_id=entry.id,
                    object_repr=force_text(entry),
                    action_flag=CHANGE,
                    change_message='{action} action initiated by user.'.format(
                        action=readable_action
                    )
                )
            except TransitionNotAllowed:
                failed_changes.append(entry.id)
            except ValueError as error:
                failed_actions.append(force_text(error))
            except AttributeError:
                failed_actions.append('{action} failed for {document}.'.format(
                    action=readable_action, document=entry
                ))

        if failed_actions:
            msg = "\n".join(failed_actions)
            self.message_user(request, msg, level=messages.ERROR)

        if failed_changes:
            failed_ids = ' '.join(map(str, failed_changes))
            msg = "The state change failed for {model_name}(s) with "\
                  "ids: {ids}".format(model_name=self._model_name.lower(),
                                      ids=failed_ids)
            self.message_user(request, msg, level=messages.ERROR)

        if not failed_actions and not failed_changes:
            qs_count = queryset.count()
            if action == 'clone_into_draft':
                results = ', '.join(result.series_number for result in results)
                msg = 'Successfully cloned {count} {model_name}(s) ' \
                      'into {results}.'.format(
                          model_name=self._model_name.lower(), count=qs_count,
                          results=results
                      )
            else:
                msg = 'Successfully changed {count} {model_name}(s).'.format(
                    model_name=self._model_name.lower(), count=qs_count
                )
            self.message_user(request, msg)

    def has_delete_permission(self, request, obj=None):
        if request.user.is_superuser:
            return True
        return False

    def get_actions(self, request):
        actions = super(BillingDocumentAdmin, self).get_actions(request)
        if not request.user.is_superuser:
            if 'delete_selected' in actions:
                del actions['delete_selected']
        return actions

    def total(self, obj):
        return '{:.2f} {currency}'.format(obj.total,
                                          currency=obj.currency)

    def transaction_total(self, obj):
        if obj.transaction_xe_rate:
            return '{:.2f} {currency}'.format(obj.total_in_transaction_currency,
                                              currency=obj.transaction_currency)
        return None

    def _download_pdf(self, url, base_path):
        local_file_path = os.path.join(base_path, 'billing-temp-document.pdf')
        response = requests.get(url, stream=True)
        should_wipe_bad_headers = True
        with open(local_file_path, 'wb') as out_file:
            for chunk in response.iter_content(chunk_size=1024):
                if chunk:
                    if should_wipe_bad_headers:
                        pdf_header_pos = chunk.find('%PDF-')
                        if pdf_header_pos > 0:
                            # The file does not start with the '%PDF-' header
                            # => trim everything up to that position
                            chunk = chunk[pdf_header_pos:]
                        should_wipe_bad_headers = False
                    out_file.write(chunk)
                    out_file.flush()

        return local_file_path

    def download_selected_documents(self, request, queryset):
        # NOTE (important): this works only if the pdf is not stored on local
        # disk as it is fetched via HTTP
        now = timezone.now()

        queryset = queryset.filter(
            state__in=[BillingDocumentBase.STATES.ISSUED,
                       BillingDocumentBase.STATES.CANCELED,
                       BillingDocumentBase.STATES.PAID]
        )

        base_path = '/tmp'
        merger = PdfFileMerger()
        for document in queryset:
            if document.pdf:
                local_file_path = self._download_pdf(document.pdf.url, base_path)
                try:
                    reader = PdfFileReader(open(local_file_path, 'rb'))
                    merger.append(reader)
                    logging_ctx = {
                        'number': document.series_number,
                        'status': 'ok'
                    }
                except Exception as e:
                    logging_ctx = {
                        'number': document.series_number,
                        'status': 'failed',
                        'error': e
                    }

                logger.debug('Admin aggregate PDF generation: %s', logging_ctx)

                try:
                    os.remove(local_file_path)
                except OSError as e:
                    if e.errno != errno.ENOENT:
                        raise

        response = HttpResponse(content_type='application/pdf')
        filename = 'Billing-Documents-{now}.pdf'.format(now=now)
        content_disposition = 'attachment; filename="{fn}'.format(fn=filename)
        response['Content-Disposition'] = content_disposition

        merger.write(response)
        merger.close()

        return response

    download_selected_documents.short_description = 'Download selected documents'

    def get_related_document(self, obj):
        return obj.related_document.admin_change_url if obj.related_document else None
    get_related_document.short_description = 'Related document'
    get_related_document.allow_tags = True


class InvoiceDocumentEntryInline(DocumentEntryInline):
    fk_name = 'invoice'


class InvoiceAdmin(BillingDocumentAdmin):
    form = InvoiceForm
    list_display = BillingDocumentAdmin.list_display + [
        'invoice_pdf',
    ]
    inlines = [InvoiceDocumentEntryInline]

    def issue(self, request, queryset):
        self.perform_action(request, queryset, 'issue')
    issue.short_description = 'Issue the selected invoice(s)'

    def pay(self, request, queryset):
        self.perform_action(request, queryset, 'pay')
    pay.short_description = 'Pay the selected invoice(s)'

    def cancel(self, request, queryset):
        self.perform_action(request, queryset, 'cancel')
    cancel.short_description = 'Cancel the selected invoice(s)'

    def clone(self, request, queryset):
        self.perform_action(request, queryset, 'clone_into_draft')
    clone.short_description = 'Clone the selected invoice(s) into draft'

    def mark_pdf_for_generation(self, request, queryset):
        self.perform_action(request, queryset, 'mark_for_generation')
    mark_pdf_for_generation.short_description = 'Mark the selected invoice(s) for PDF generation'

    def invoice_pdf(self, invoice):
        if invoice.pdf:
            url = reverse('invoice-pdf', kwargs={'invoice_id': invoice.id})
            return '<a href="{url}" target="_blank">{url}</a>'.format(url=url)
        else:
            return None
    invoice_pdf.allow_tags = True

    @property
    def _model(self):
        return Invoice

    @property
    def _model_name(self):
        return "Invoice"


class ProformaDocumentEntryInline(DocumentEntryInline):
    fk_name = 'proforma'


class ProformaAdmin(BillingDocumentAdmin):
    form = ProformaForm
    list_display = BillingDocumentAdmin.list_display + [
        'proforma_pdf',
    ]
    inlines = [ProformaDocumentEntryInline]
    actions = BillingDocumentAdmin.actions + ['create_invoice']

    def issue(self, request, queryset):
        self.perform_action(request, queryset, 'issue')
    issue.short_description = 'Issue the selected proforma(s)'

    def create_invoice(self, request, queryset):
        self.perform_action(request, queryset, 'create_invoice')
    create_invoice.short_description = 'Create invoice from proforma(s)'

    def pay(self, request, queryset):
        self.perform_action(request, queryset, 'pay')
    pay.short_description = 'Pay the selected proforma(s)'

    def cancel(self, request, queryset):
        self.perform_action(request, queryset, 'cancel')
    cancel.short_description = 'Cancel the selected proforma(s)'

    def clone(self, request, queryset):
        self.perform_action(request, queryset, 'clone_into_draft')
    clone.short_description = 'Clone the selected proforma(s) into draft'

    def mark_pdf_for_generation(self, request, queryset):
        self.perform_action(request, queryset, 'mark_for_generation')
    mark_pdf_for_generation.short_description = 'Mark the selected proforma(s) for PDF generation'

    def proforma_pdf(self, proforma):
        if proforma.pdf:
            url = reverse('proforma-pdf', kwargs={'proforma_id': proforma.id})
            return '<a href="{url}" target="_blank">{url}</a>'.format(url=url)
        else:
            return None
    proforma_pdf.allow_tags = True

    @property
    def _model(self):
        return Proforma

    @property
    def _model_name(self):
        return "Proforma"


class TransactionForm(forms.ModelForm):
    class Meta:
        model = Transaction
        fields = ['proforma', 'invoice', 'amount', 'currency', 'state',
                  'payment_method', 'uuid', 'valid_until', 'last_access',
                  'data', 'fail_code', 'cancel_code', 'refund_code']

        readonly_fields = ['state', 'uuid', 'last_access']
        create_only_fields = ['amount', 'currency', 'proforma', 'invoice',
                              'payment_method', 'valid_until']

        widgets = {
            'invoice': autocomplete.ModelSelect2(
                url='autocomplete-invoice'
            ),
            'proforma': autocomplete.ModelSelect2(
                url='autocomplete-proforma'
            ),
            'payment_method': autocomplete.ModelSelect2(
                url='autocomplete-payment-method'
            ),

        }

    def __init__(self, *args, **kwargs):
        super(TransactionForm, self).__init__(*args, **kwargs)

        if 'amount' in self.fields:
            self.fields['amount'].required = False
        if 'currency' in self.fields:
            self.fields['currency'].required = False

    def clean(self):
        if ((self.cleaned_data['amount'] and not self.cleaned_data['currency']) or
                (self.cleaned_data['amount'] is None and self.cleaned_data['currency'])):
            raise ValidationError('You must either specify both amount and currency fields or '
                                  'leave them both blank.')

        document = self.cleaned_data['proforma'] or self.cleaned_data['invoice']
        if not self.cleaned_data['currency']:
            self.cleaned_data['amount'] = document.amount_to_be_charged_in_transaction_currency
            self.cleaned_data['currency'] = document.transaction_currency

        return self.cleaned_data


class TransactionAdmin(ModelAdmin):
    form = TransactionForm

    list_display = ('uuid', 'related_invoice', 'related_proforma',
                    'amount', 'state', 'created_at', 'updated_at',
                    'get_customer', 'get_pay_url', 'get_payment_method',
                    'get_is_recurring')
    list_filter = ('payment_method__customer', 'state',
                   'payment_method__payment_processor')
    actions = ['execute', 'process', 'cancel', 'settle', 'fail']
    ordering = ['-created_at']

    def get_queryset(self, request):
        return super(TransactionAdmin, self).get_queryset(request) \
                                            .select_related('payment_method__customer',
                                                            'invoice', 'proforma')

    def get_readonly_fields(self, request, instance=None):
        if instance:
            return self.form.Meta.readonly_fields + self.form.Meta.create_only_fields
        return self.form.Meta.readonly_fields

    def get_pay_url(self, obj):
        return u'<a href="%s">%s</a>' % (get_payment_url(obj, None),
                                         obj.payment_processor)

    get_pay_url.allow_tags = True
    get_pay_url.short_description = 'Pay URL'

    def get_customer(self, obj):
        link = urlresolvers.reverse("admin:silver_customer_change",
                                    args=[obj.payment_method.customer.pk])
        return u'<a href="%s">%s</a>' % (link, obj.payment_method.customer)
    get_customer.allow_tags = True
    get_customer.short_description = 'Customer'

    def get_is_recurring(self, obj):
        return obj.payment_method.verified
    get_is_recurring.boolean = True
    get_is_recurring.short_description = 'Recurring'

    def get_payment_method(self, obj):
        link = urlresolvers.reverse("admin:silver_paymentmethod_change",
                                    args=[obj.payment_method.pk])
        return u'<a href="%s">%s</a>' % (link, obj.payment_method)
    get_payment_method.allow_tags = True
    get_payment_method.short_description = 'Payment Method'

    def perform_action(self, request, queryset, action, display_verb=None):
        failed_count = 0
        transactions_count = len(queryset)

        if action not in self.actions:
            self.message_user(request, 'Illegal action.', level=messages.ERROR)
            return

        method = getattr(Transaction, action)

        for transaction in queryset:
            try:
                method(transaction)
                transaction.save()
            except TransitionNotAllowed:
                failed_count += 1

        settled_count = transactions_count - failed_count

        if not failed_count:
            self.message_user(
                request,
                'Successfully %s %d transactions.' % (
                    display_verb or action, transactions_count
                )
            )
        elif failed_count != transactions_count:

            self.message_user(
                request,
                '%s %d transactions, %d failed.' % (
                    display_verb or action, settled_count, failed_count
                ),
                level=messages.WARNING
            )
        else:
            self.message_user(
                request,
                'Couldn\'t %s any of the selected transactions.' % action,
                level=messages.ERROR
            )

        action = action.capitalize()

        logger.info('[Admin][%s Transaction]: %s', action, {
            'detail': '%s Transaction action initiated by user.' % action,
            'user_id': request.user.id,
            'user_staff': request.user.is_staff,
            'failed_count': failed_count,
            'settled_count': settled_count
        })

    def execute(self, request, queryset):
        failed_count = 0
        transactions_count = len(queryset)

        for transaction in queryset:
            try:
                payment_processor = transaction.payment_method.get_payment_processor()
                if payment_processor.type != PaymentProcessorTypes.Triggered:
                    continue
                payment_processor.process_transaction(transaction)
            except Exception:
                failed_count += 1
                logger.error('Encountered exception while executing transaction '
                             'with id=%s.', transaction.id, exc_info=True)

        settled_count = transactions_count - failed_count

        if not failed_count:
            self.message_user(
                request,
                'Successfully executed %d transactions.' % (transactions_count)
            )
        elif failed_count != transactions_count:
            self.message_user(
                request,
                'Executed %d transactions, %d failed.' % (
                    settled_count, failed_count
                ),
                level=messages.WARNING
            )
        else:
            self.message_user(
                request,
                'Couldn\'t execute any of the selected transactions.',
                level=messages.ERROR
            )

        logger.info('[Admin][%s Transaction]: Execute', {
            'detail': 'Execute Transaction action initiated by user.',
            'user_id': request.user.id,
            'user_staff': request.user.is_staff,
            'failed_count': failed_count,
            'settled_count': settled_count
        })
    execute.short_description = 'Execute the selected transactions'

    def process(self, request, queryset):
        self.perform_action(request, queryset, 'process', 'processed')
    process.short_description = 'Process the selected transactions'

    def cancel(self, request, queryset):
        self.perform_action(request, queryset, 'cancel', 'canceled')
    cancel.short_description = 'Cancel the selected transactions'

    def settle(self, request, queryset):
        self.perform_action(request, queryset, 'settle', 'settled')
    settle.short_description = 'Settle the selected transactions'

    def fail(self, request, queryset):
        self.perform_action(request, queryset, 'fail', 'failed')
    fail.short_description = 'Fail the selected transactions'

    def related_invoice(self, obj):
        return obj.invoice.admin_change_url if obj.invoice else None
    related_invoice.allow_tags = True
    related_invoice.short_description = 'Invoice'

    def related_proforma(self, obj):
        return obj.proforma.admin_change_url if obj.proforma else None
    related_proforma.allow_tags = True
    related_proforma.short_description = 'Proforma'


class PaymentMethodAdmin(ModelAdmin):
    list_display = ('customer', 'payment_processor', 'added_at', 'verified',
                    'canceled')
    list_filter = ('customer', 'verified', 'canceled',
                   'payment_processor')
    search_fields = ['customer__first_name', 'customer__last_name',
                     'customer__company']


site.register(Transaction, TransactionAdmin)
site.register(PaymentMethod, PaymentMethodAdmin)
site.register(Plan, PlanAdmin)
site.register(Subscription, SubscriptionAdmin)
site.register(Customer, CustomerAdmin)
site.register(Provider, ProviderAdmin)
site.register(Invoice, InvoiceAdmin)
site.register(Proforma, ProformaAdmin)
site.register(ProductCode)
site.register(MeteredFeature)
