import csv
import json
import logging
from datetime import timedelta

from django.contrib import messages
from django.core.urlresolvers import reverse
from django.db.models import Count, Q
from django.db.models.functions import Concat
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import ugettext as _
from django.views.generic import DetailView, ListView, View

from pretix.base.models import Order, Quota
from pretix.base.services.mail import SendMailException
from pretix.base.services.orders import mark_order_paid
from pretix.base.settings import SettingsSandbox
from pretix.control.permissions import (
    EventPermissionRequiredMixin, OrganizerPermissionRequiredMixin,
)
from pretix.control.views.organizer import OrganizerDetailViewMixin
from pretix.plugins.banktransfer import csvimport, mt940import
from pretix.plugins.banktransfer.models import BankImportJob, BankTransaction
from pretix.plugins.banktransfer.tasks import process_banktransfers

logger = logging.getLogger('pretix.plugins.banktransfer')


class ActionView(View):
    permission = 'can_change_orders'

    def _discard(self, trans):
        trans.state = BankTransaction.STATE_DISCARDED
        trans.shred_private_data()
        trans.save()
        return JsonResponse({
            'status': 'ok'
        })

    def _retry(self, trans):
        if trans.amount != trans.order.total:
            return JsonResponse({
                'status': 'error',
                'message': _('The transaction amount is incorrect.')
            })
        return self._accept_ignore_amount(trans)

    def _accept_ignore_amount(self, trans):
        if trans.order.status == Order.STATUS_PAID:
            return JsonResponse({
                'status': 'error',
                'message': _('The order is already marked as paid.')
            })
        elif trans.order.status == Order.STATUS_REFUNDED:
            return JsonResponse({
                'status': 'error',
                'message': _('The order has already been refunded.')
            })
        elif trans.order.status == Order.STATUS_CANCELED:
            return JsonResponse({
                'status': 'error',
                'message': _('The order has already been canceled.')
            })

        try:
            mark_order_paid(trans.order, provider='banktransfer', info=json.dumps({
                'reference': trans.reference,
                'date': trans.date,
                'payer': trans.payer,
                'trans_id': trans.pk
            }), count_waitinglist=False)
            trans.state = BankTransaction.STATE_VALID
            trans.save()
        except Quota.QuotaExceededException as e:
            return JsonResponse({
                'status': 'error',
                'message': str(e)
            })
        except SendMailException:
            return JsonResponse({
                'status': 'error',
                'message': _('Problem sending email.')
            })
        else:
            trans.state = BankTransaction.STATE_VALID
            trans.save()
            return JsonResponse({
                'status': 'ok'
            })

    def _assign(self, trans, code):
        try:
            if '-' in code:
                trans.order = self.order_qs().get(code=code.split('-')[1], event__slug__iexact=code.split('-')[0])
            else:
                trans.order = self.order_qs().get(code=code.split('-')[-1])
        except Order.DoesNotExist:
            return JsonResponse({
                'status': 'error',
                'message': _('Unknown order code')
            })
        else:
            return self._retry(trans)

    def _comment(self, trans, comment):
        from pretix.base.templatetags.rich_text import rich_text
        trans.comment = comment
        trans.save()
        return JsonResponse({
            'status': 'ok',
            'comment': rich_text(comment),
            'plain': comment,
        })

    def post(self, request, *args, **kwargs):
        for k, v in request.POST.items():
            if not k.startswith('action_'):
                continue
            if 'event' in kwargs:
                trans = get_object_or_404(BankTransaction, id=k.split('_')[1], event=request.event)
            else:
                trans = get_object_or_404(BankTransaction, id=k.split('_')[1], organizer=request.organizer)

            if v == 'discard' and trans.state in (BankTransaction.STATE_INVALID, BankTransaction.STATE_ERROR,
                                                  BankTransaction.STATE_NOMATCH, BankTransaction.STATE_DUPLICATE):
                return self._discard(trans)

            elif v == 'accept' and trans.state == BankTransaction.STATE_INVALID:
                # Accept anyway even with wrong amount
                return self._accept_ignore_amount(trans)

            elif v.startswith('comment:'):
                return self._comment(trans, v[8:])

            elif v.startswith('assign:') and trans.state == BankTransaction.STATE_NOMATCH:
                return self._assign(trans, v[7:])

            elif v == 'retry' and trans.state in (BankTransaction.STATE_ERROR, BankTransaction.STATE_DUPLICATE):
                return self._retry(trans)

            return JsonResponse({
                'status': 'error',
                'message': 'Unknown action'
            })

    def get(self, request, *args, **kwargs):
        from django.utils.formats import localize

        u = request.GET.get('query', '')
        if len(u) < 2:
            return JsonResponse({'results': []})

        if "-" in u:
            code = (Q(event__slug__icontains=u.split("-")[0])
                    & Q(code__icontains=Order.normalize_code(u.split("-")[1])))
        else:
            code = Q(code__icontains=Order.normalize_code(u))
        qs = self.order_qs().order_by('pk').annotate(inr=Concat('invoices__prefix', 'invoices__invoice_no')).filter(
            code
            | Q(email__icontains=u)
            | Q(positions__attendee_name__icontains=u)
            | Q(positions__attendee_email__icontains=u)
            | Q(invoice_address__name__icontains=u)
            | Q(invoice_address__company__icontains=u)
            | Q(invoices__invoice_no=u)
            | Q(invoices__invoice_no=u.zfill(5))
            | Q(inr=u)
        ).select_related('event').annotate(pcnt=Count('invoices')).distinct()
        # Yep, we wouldn't need to count the invoices here. However, having this Count() statement in there
        # tricks Django into generating a GROUP BY clause that it otherwise wouldn't and that is required to
        # avoid duplicate results. Yay?

        return JsonResponse({
            'results': [
                {
                    'code': o.event.slug.upper() + '-' + o.code,
                    'status': o.get_status_display(),
                    'total': localize(o.total) + ' ' + o.event.currency
                } for o in qs
            ]
        })

    def order_qs(self):
        return self.request.event.orders


class JobDetailView(DetailView):
    template_name = 'pretixplugins/banktransfer/job_detail.html'
    permission = 'can_change_orders'
    context_objectname = 'job'

    def redirect_form(self):
        kwargs = {
            'organizer': self.request.organizer.slug,
        }
        if 'event' in self.kwargs:
            kwargs['event'] = self.kwargs['event']
        return redirect(reverse('plugins:banktransfer:import', kwargs=kwargs))

    def redirect_back(self):
        kwargs = {
            'organizer': self.request.organizer.slug,
            'job': self.kwargs['job']
        }
        if 'event' in self.kwargs:
            kwargs['event'] = self.kwargs['event']
        return redirect(reverse('plugins:banktransfer:import.job', kwargs=kwargs))

    @cached_property
    def job(self):
        if 'event' in self.kwargs:
            kwargs = {'event': self.request.event}
        else:
            kwargs = {'organizer': self.request.organizer}
        return get_object_or_404(BankImportJob, id=self.kwargs['job'], **kwargs)

    def get(self, request, *args, **kwargs):
        if 'ajax' in request.GET:
            return JsonResponse({
                'state': self.job.state
            })

        context = self.get_context_data()
        return self.render_to_response(context)

    def get_context_data(self, **kwargs):
        ctx = {}

        qs = self.job.transactions.select_related('order', 'order__event')

        ctx['transactions_valid'] = qs.filter(state=BankTransaction.STATE_VALID).count()
        ctx['transactions_invalid'] = qs.filter(state__in=[
            BankTransaction.STATE_INVALID, BankTransaction.STATE_ERROR
        ]).count()
        ctx['transactions_ignored'] = qs.filter(state__in=[
            BankTransaction.STATE_DUPLICATE, BankTransaction.STATE_NOMATCH
        ]).count()
        ctx['job'] = self.job
        ctx['organizer'] = self.request.organizer

        if 'event' in self.kwargs:
            ctx['basetpl'] = 'pretixplugins/banktransfer/import_base.html'
        else:
            ctx['basetpl'] = 'pretixplugins/banktransfer/import_base_organizer.html'

        return ctx


class ImportView(ListView):
    template_name = 'pretixplugins/banktransfer/import_form.html'
    permission = 'can_change_orders'
    context_object_name = 'transactions_unhandled'
    paginate_by = 30

    def get_queryset(self):
        if 'event' in self.kwargs:
            qs = BankTransaction.objects.filter(
                Q(event=self.request.event)
            )
        else:
            qs = BankTransaction.objects.filter(
                Q(organizer=self.request.organizer)
            )
        qs = qs.select_related('order').filter(state__in=[
            BankTransaction.STATE_INVALID, BankTransaction.STATE_ERROR,
            BankTransaction.STATE_DUPLICATE, BankTransaction.STATE_NOMATCH
        ])
        if 'search' in self.request.GET:
            q = self.request.GET.get('search')
            qs = qs.filter(
                Q(payer__icontains=q) | Q(reference__icontains=q) | Q(comment__icontains=q)
            ).order_by(
                '-import_job__created'
            )

        return qs

    def discard_all(self):
        self.get_queryset().update(payer='', reference='', state=BankTransaction.STATE_DISCARDED)
        messages.success(self.request, _('All unresolved transactions have been discarded.'))

    def post(self, *args, **kwargs):
        if self.request.POST.get('discard', '') == 'all':
            self.discard_all()
            return self.redirect_back()

        elif ('file' in self.request.FILES and '.csv' in self.request.FILES.get('file').name.lower()) \
                or 'amount' in self.request.POST:
            # Process CSV
            return self.process_csv()

        elif 'file' in self.request.FILES and (
            '.txt' in self.request.FILES.get('file').name.lower()
            or '.sta' in self.request.FILES.get('file').name.lower()
        ):
            return self.process_mt940()

        elif self.request.FILES.get('file') is None:
            messages.error(self.request, _('You must choose a file to import.'))
            return self.redirect_back()

        else:
            messages.error(self.request, _('We were unable to detect the file type of this import. Please '
                                           'contact support for help.'))
            return self.redirect_back()

    @cached_property
    def settings(self):
        return SettingsSandbox('payment', 'banktransfer', getattr(self.request, 'event', self.request.organizer))

    def process_mt940(self):
        try:
            return self.start_processing(mt940import.parse(self.request.FILES.get('file')))
        except:
            logger.exception('Failed to import MT940 file')
            messages.error(self.request, _('We were unable to process your input.'))
            return self.redirect_back()

    def process_csv_file(self):
        o = getattr(self.request, 'event', self.request.organizer)
        try:
            data = csvimport.get_rows_from_file(self.request.FILES['file'])
        except csv.Error as e:  # TODO: narrow down
            logger.error('Import failed: ' + str(e))
            messages.error(self.request, _('I\'m sorry, but we were unable to import this CSV file. Please '
                                           'contact support for help.'))
            return self.redirect_back()

        if len(data) == 0:
            messages.error(self.request, _('I\'m sorry, but we detected this file as empty. Please '
                                           'contact support for help.'))

        if o.settings.get('banktransfer_csvhint') is not None:
            hint = o.settings.get('banktransfer_csvhint', as_type=dict)

            try:
                parsed, good = csvimport.parse(data, hint)
            except csvimport.HintMismatchError:  # TODO: narrow down
                logger.exception('Import using stored hint failed')
            else:
                if good:
                    return self.start_processing(parsed)

        return self.assign_view(data)

    def process_csv_hint(self):
        data = []
        for i in range(int(self.request.POST.get('rows'))):
            data.append(
                [
                    self.request.POST.get('col[%d][%d]' % (i, j))
                    for j in range(int(self.request.POST.get('cols')))
                ]
            )
        if 'reference' not in self.request.POST:
            messages.error(self.request, _('You need to select the column containing the payment reference.'))
            return self.assign_view(data)
        try:
            hint = csvimport.new_hint(self.request.POST)
        except Exception as e:
            logger.error('Parsing hint failed: ' + str(e))
            messages.error(self.request, _('We were unable to process your input.'))
            return self.assign_view(data)
        o = getattr(self.request, 'event', self.request.organizer)
        try:
            o.settings.set('banktransfer_csvhint', hint)
        except Exception as e:  # TODO: narrow down
            logger.error('Import using stored hint failed: ' + str(e))
            pass
        else:
            parsed, __ = csvimport.parse(data, hint)
            return self.start_processing(parsed)

    def process_csv(self):
        if 'file' in self.request.FILES:
            return self.process_csv_file()
        elif 'amount' in self.request.POST:
            return self.process_csv_hint()
        return super().get(self.request)

    def assign_view(self, parsed):
        ctx = {'rows': parsed}
        if 'event' in self.kwargs:
            ctx['basetpl'] = 'pretixplugins/banktransfer/import_base.html'
        else:
            ctx['basetpl'] = 'pretixplugins/banktransfer/import_base_organizer.html'
            ctx['organizer'] = self.request.organizer
        return render(self.request, 'pretixplugins/banktransfer/import_assign.html', ctx)

    @cached_property
    def job_running(self):
        if 'event' in self.kwargs:
            qs = BankImportJob.objects.filter(
                Q(event=self.request.event) | Q(organizer=self.request.organizer)
            )
        else:
            qs = BankImportJob.objects.filter(
                Q(organizer=self.request.organizer)
            )
        return qs.filter(
            state=BankImportJob.STATE_RUNNING,
            created__lte=now() - timedelta(minutes=30)  # safety timeout
        ).first()

    def redirect_back(self):
        kwargs = {
            'organizer': self.request.organizer.slug
        }
        if 'event' in self.kwargs:
            kwargs['event'] = self.kwargs['event']
        return redirect(reverse('plugins:banktransfer:import', kwargs=kwargs))

    def start_processing(self, parsed):
        if self.job_running:
            messages.error(self.request,
                           _('An import is currently being processed, please try again in a few minutes.'))
            return self.redirect_back()
        if 'event' in self.kwargs:
            job = BankImportJob.objects.create(event=self.request.event, organizer=self.request.organizer)
        else:
            job = BankImportJob.objects.create(organizer=self.request.organizer)
        process_banktransfers.apply_async(kwargs={
            'job': job.pk,
            'data': parsed
        })
        kwargs = {
            'organizer': self.request.organizer.slug,
            'job': job.pk
        }
        if 'event' in self.kwargs:
            kwargs['event'] = self.kwargs['event']
        return redirect(reverse('plugins:banktransfer:import.job', kwargs=kwargs))

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data()
        ctx['job_running'] = self.job_running
        ctx['no_more_payments'] = False

        if 'event' in self.kwargs:
            ctx['basetpl'] = 'pretixplugins/banktransfer/import_base.html'
            if not self.request.event.has_subevents and self.request.event.settings.get('payment_term_last'):
                if now() > self.request.event.payment_term_last:
                    ctx['no_more_payments'] = True
        else:
            ctx['basetpl'] = 'pretixplugins/banktransfer/import_base_organizer.html'
            ctx['organizer'] = self.request.organizer
        return ctx


class OrganizerBanktransferView:
    def dispatch(self, request, *args, **kwargs):
        if len(request.organizer.events.order_by('currency').values_list('currency', flat=True).distinct()) > 1:
            messages.error(request, _('Please perform per-event bank imports as this organizer has events with '
                                      'multiple currencies.'))
            return redirect('control:organizer', organizer=request.organizer.slug)
        return super().dispatch(request, *args, **kwargs)


class EventImportView(EventPermissionRequiredMixin, ImportView):
    permission = 'can_change_orders'


class OrganizerImportView(OrganizerBanktransferView, OrganizerPermissionRequiredMixin, OrganizerDetailViewMixin,
                          ImportView):
    permission = 'can_change_orders'


class EventJobDetailView(EventPermissionRequiredMixin, JobDetailView):
    permission = 'can_change_orders'


class OrganizerJobDetailView(OrganizerBanktransferView, OrganizerPermissionRequiredMixin, OrganizerDetailViewMixin,
                             JobDetailView):
    permission = 'can_change_orders'


class EventActionView(EventPermissionRequiredMixin, ActionView):
    permission = 'can_change_orders'


class OrganizerActionView(OrganizerBanktransferView, OrganizerPermissionRequiredMixin, OrganizerDetailViewMixin,
                          ActionView):
    permission = 'can_change_orders'

    def order_qs(self):
        all = self.request.user.teams.filter(organizer=self.request.organizer, can_change_orders=True,
                                             can_view_orders=True, all_events=True).exists()
        if self.request.user.is_superuser or all:
            return Order.objects.filter(event__organizer=self.request.organizer)
        else:
            return Order.objects.filter(
                event_id__in=self.request.user.teams.filter(
                    organizer=self.request.organizer, can_change_orders=True, can_view_orders=True
                ).values_list('limit_events__id', flat=True)
            )
