# barbacompta - invoicing for dummies
# Copyright (C) 2019-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.


import datetime as dt
import re

from adminsortable2.admin import SortableInlineAdminMixin
from django import http
from django.conf.urls import url
from django.contrib import admin
from django.contrib.admin.options import BaseModelAdmin
from django.contrib.humanize.templatetags.humanize import ordinal
from django.db.models import TextField
from django.forms import Textarea
from django.forms.models import BaseInlineFormSet
from django.shortcuts import render
from django.urls import reverse
from django.utils.html import format_html
from django.utils.six import BytesIO

import eo_gestion.admin

from .. import actions, ods
from . import forms, models, views
from .templatetags.eo_facture import amountformat


class LookupAllowed:
    def lookup_allowed(self, *args, **kwargs):
        return True


class SelectRelatedMixin:
    def get_queryset(self, request):
        qs = super().get_queryset(request)
        return qs.select_related()


class CommonPaymentInline(BaseModelAdmin):
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == 'facture' and request.path.endswith('/add/'):
            kwargs['queryset'] = models.Facture.objects.avec_solde()
        if db_field.name == 'ligne_banque_pop' and request.path.endswith('/add/'):
            kwargs['queryset'] = models.encaissements_avec_solde_non_affecte()
        return super().formfield_for_foreignkey(db_field, request, **kwargs)


class PrestationInline(SelectRelatedMixin, admin.TabularInline):
    model = models.Prestation
    form = forms.PrestationForm


class FactureInline(SelectRelatedMixin, admin.TabularInline):
    model = models.Facture
    form = forms.FactureForm


class PaymentInlineFormset(BaseInlineFormSet):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for form in self.forms:
            if form.instance.id is None:
                field = form.fields["ligne_banque_pop"]
                if not hasattr(field, "parent_instance"):
                    field.queryset = models.encaissements_avec_solde_non_affecte()
                field = form.fields["facture"]
                if not hasattr(field, "parent_instance"):
                    field.queryset = models.Facture.objects.avec_solde()


class PaymentInline(SelectRelatedMixin, admin.TabularInline, CommonPaymentInline):
    form = forms.PaymentForm

    model = models.Payment
    list_display = ['facture', 'ligne_banque_pop', 'montant_affecte']
    formset = PaymentInlineFormset
    extra = 0


class LigneInline(SelectRelatedMixin, SortableInlineAdminMixin, admin.TabularInline):
    form = forms.LigneForm
    model = models.Ligne
    original = False
    verbose_name_plural = "Lignes de facture (vous pouvez les réordonner par drag&drop)"
    formfield_overrides = {
        TextField: {
            'widget': Textarea(attrs={'cols': 30, 'rows': 1}),
        }
    }


class ActiveFilter(admin.SimpleListFilter):
    title = 'statut'
    parameter_name = 'active'

    def value(self):
        value = super().value()
        return 'True' if value in (None, True) else value

    def choices(self, changelist):
        choices = list(super().choices(changelist))
        return choices[1:]  # don't include automatic "All"

    def lookups(self, request, model_admin):
        return [(True, 'actif'), (False, 'inactif'), ('all', 'tous')]

    def queryset(self, request, queryset):
        if self.value() == 'all':
            return queryset
        return queryset.filter(active=bool(self.value() == 'True'))


class ClientAdmin(admin.ModelAdmin):
    form = forms.ClientForm
    list_display = ['nom', 'adresse', 'email', 'telephone']
    list_editable = ['email', 'telephone']
    list_filter = [ActiveFilter]
    ordering = ['nom']
    search_fields = ['nom', 'email']
    save_on_top = True
    list_select_related = True
    raw_id_fields = ['chorus_structure']

    def get_readonly_fields(self, request, obj=None):
        readonly_fields = super().get_readonly_fields(request, obj=obj)
        if obj and obj.chorus_structure:
            readonly_fields += ('siret', 'service_code')
        return readonly_fields

    def save_model(self, request, obj, form, change):
        if not obj.id:
            obj.creator = request.user
        obj.save()

    class Media:
        css = {'all': ('css/client.css',)}


def show_client(obj):
    url = reverse('admin:eo_facture_client_change', args=[obj.client.id])
    return format_html('<a href="{0}">{1}</a>', url, obj.client)


show_client.short_description = 'Client'
show_client.admin_order_field = 'client'


class ContratAdmin(LookupAllowed, admin.ModelAdmin):
    form = forms.ContratForm
    inlines = [
        PrestationInline,
    ]
    list_display = [
        "__str__",
        show_client,
        'column_montant',
        'pourcentage_facture',
        'creation',
        'creator',
        'tag_list',
    ]
    list_filter = ['tags', 'client']
    list_select_related = True
    save_on_top = True
    search_fields = ['intitule', 'client__nom', 'tags__name']
    readonly_fields = ['creator']
    list_select_related = True
    actions = [actions.export_references_as_fodt]
    autocomplete_fields = ['client']

    def column_montant(self, obj):
        return amountformat(obj.montant())

    column_montant.short_description = 'Montant'

    def get_queryset(self, request):
        qs = super().get_queryset(request)
        return qs.prefetch_related('prestations', 'factures__lignes', 'tags')

    def tag_list(self, obj):
        return ', '.join(o.name for o in obj.tags.all())

    def save_model(self, request, obj, form, change):
        if not obj.id:
            obj.creator = request.user
        obj.save()

    class Media:
        css = {'all': ('css/contrat.css',)}

    def duplicate(self, request, contract):
        contrat = models.Contrat.objects.get(id=contract)
        if request.method == 'POST':
            form = forms.DuplicateContractForm(request=request, data=request.POST)
            if form.is_valid():
                return http.HttpResponseRedirect(
                    reverse('admin:eo_facture_contrat_change', args=[form.cleaned_data['new_contrat'].id])
                )
        else:
            new_intitule = contrat.intitule
            new_intitule += ' dupliqué le %s' % dt.datetime.now()
            form = forms.DuplicateContractForm(initial={'contrat': contrat.id, 'new_intitule': new_intitule})
        context = {
            'form': form,
            'contrat': contrat,
            'app_label': self.model._meta.app_label,
            'has_change_permission': self.has_change_permission(request),
            'opts': self.model._meta,
        }

        return render(request, "admin/eo_facture/contrat/duplicate.html", context=context)

    def get_urls(self):
        urls = super().get_urls()
        duplicate_view = self.admin_site.admin_view(self.duplicate)
        my_urls = [url(r'^(.+)/duplicate/$', duplicate_view, name='eo_facture_contrat_duplicate')]
        return my_urls + urls


def index(facture):
    return format_html('{0}', ordinal(facture.index()))


index.short_description = "Ordre"


class FactureAdmin(LookupAllowed, admin.ModelAdmin):
    form = forms.FactureForm
    inlines = [LigneInline, PaymentInline]
    list_display = [
        'code',
        index,
        'show_contrat',
        'show_client',
        'emission',
        'creator',
        'column_montant',
        'column_montant_ttc',
        'column_solde',
        'paid',
    ]
    list_filter = ['paid', 'emission', 'creator', 'client']
    fields = [
        'proforma',
        'ordre',
        'client',
        'contrat',
        'intitule',
        'numero_engagement',
        'notes',
        'private_notes',
        'taux_tva',
        'emission',
        'echeance',
        'paid',
        'solde',
        'montant',
        'montant_ttc',
        'account_on_previous_period',
    ]
    readonly_fields = ['creator', 'solde', 'ordre', 'montant', 'montant_ttc']
    date_hierarchy = 'emission'
    list_select_related = True
    save_on_top = True
    search_fields = ['contrat__intitule', 'contrat__client__nom']
    list_select_related = True
    autocomplete_fields = ['client']

    def column_montant(self, obj):
        return amountformat(obj.montant)

    column_montant.short_description = 'Montant'

    def column_montant_ttc(self, obj):
        return amountformat(obj.montant_ttc)

    column_montant_ttc.short_description = 'Montant TTC'

    def column_solde(self, obj):
        return amountformat(obj.solde())

    column_solde.short_description = 'Solde'

    def get_queryset(self, request):
        from django.db import connection

        qs = super().get_queryset(request)
        qs = qs.prefetch_related("lignes__facture__client")
        qs = qs.prefetch_related("payments__ligne_banque_pop")
        qs = qs.prefetch_related("contrat__factures")
        if connection.vendor == "postgresql":
            qs = qs.extra({"year": "EXTRACT(year FROM emission)"})
        elif connection.vendor == "sqlite":
            qs = qs.extra({"year": "strftime('%Y', emission)"})
        else:
            qs = qs.extra({"year": "YEAR(emission)"})
        qs = qs.order_by("-year", "-proforma", "-ordre")
        return qs

    def save_model(self, request, obj, form, change):
        if not obj.id:
            obj.creator = request.user
        obj.save()

    def add_simple(self, request):
        context = {}
        if request.method == "POST":
            form = forms.RapidFactureForm(request=request, data=request.POST)
            if form.is_valid():
                return http.HttpResponseRedirect(
                    reverse('admin:eo_facture_facture_change', args=[form.cleaned_data['facture'].id])
                )
        else:
            form = forms.RapidFactureForm(initial=request.GET)
        context = {
            'form': form,
            'app_label': self.model._meta.app_label,
            'has_change_permission': self.has_change_permission(request),
            'opts': self.model._meta,
        }

        return render(request, "admin/eo_facture/facture/add_simple.html", context=context)

    def sheet(self, request):
        workbook = ods.Workbook()
        ws = workbook.add_sheet("Factures")
        for i, header_name in enumerate(
            ['Référence', 'Nom client', 'Date', 'Montant HT', 'Montant TTC', 'TVA']
        ):
            ws.write(0, i, header_name)

        qs = self.get_queryset(request)
        qs = sorted(qs, key=lambda i: (i.emission.year, i.ordre or -1, i.emission))

        for j, facture in enumerate(qs):
            ws.write(j + 1, 0, facture.code())
            ws.write(j + 1, 1, facture.client.nom)
            ws.write(j + 1, 2, facture.emission)
            ws.write(j + 1, 3, facture.montant)
            ws.write(j + 1, 4, facture.montant_ttc)
            ws.write(j + 1, 5, facture.tva)
        stream = BytesIO()
        workbook.save(stream)
        response = http.HttpResponse(
            stream.getvalue(), content_type='application/vnd.oasis.opendocument.spreadsheet'
        )
        response['Content-Disposition'] = 'attachment; filename="factures.ods"'
        return response

    def get_urls(self):
        urls = super().get_urls()
        my_urls = [
            url(r'^view/([^/]*)', views.facture),
            url(
                r'^add_simple/$',
                self.admin_site.admin_view(self.add_simple),
                name='eo_facture_facture_add_simple',
            ),
            url(
                r'^(.+)/view_pdf/',
                self.admin_site.admin_view(views.facture_pdf),
                name='eo_facture_facture_print',
            ),
            url(r'^(.+)/view/$', self.admin_site.admin_view(views.facture), name='eo_facture_facture_html'),
            url(
                r'^(.+)/send-to-chorus/',
                self.admin_site.admin_view(views.send_to_chorus),
                name='eo_facture_facture_send_to_chorus',
            ),
            url(
                r"^sheet/$",
                self.admin_site.admin_view(self.sheet),
                name="eo_facture_facture_sheet",
            ),
            url(
                r'^(.+)/cancel/',
                self.admin_site.admin_view(views.cancel),
                name='eo_facture_facture_cancel',
            ),
        ]
        return my_urls + urls

    def show_client(self, obj):
        url = reverse('admin:eo_facture_client_change', args=[obj.client.id])
        return format_html('<a href="{0}">{1}</a>', url, obj.client)

    show_client.short_description = 'Client'

    def show_contrat(self, obj):
        if obj.contrat:
            url = reverse('admin:eo_facture_contrat_change', args=[obj.contrat.id])
            return format_html('<a href="{0}">{1}</a>', url, obj.contrat)
        return None

    show_contrat.short_description = 'Contrat'

    # adapt get_object and get_changelist_instance to produce and accept URL
    # with facture's code
    FACTURE_RE = re.compile('^F(?P<year>20[0-9]{2})(?P<ordre>[0-9]{4})$')

    def get_object(self, request, object_id, from_field=None):
        m = self.FACTURE_RE.match(object_id)
        if m:
            queryset = self.get_queryset(request)
            model = queryset.model
            year = int(m.group('year'))
            ordre = int(m.group('ordre'))
            try:
                return queryset.get(emission__year=year, ordre=ordre)
            except model.DoesNotExist:
                pass
        return super().get_object(request, object_id, from_field=from_field)

    def get_changelist_instance(self, request):
        changelist = super().get_changelist_instance(request)
        changelist.pk_attname = 'pk_or_code'
        return changelist


class PaymentAdmin(LookupAllowed, admin.ModelAdmin, CommonPaymentInline):
    form = forms.PaymentForm
    list_display = ['facture', 'ligne_banque_pop', 'montant_affecte']
    list_filter = ['ligne_banque_pop__date_valeur', 'facture', 'facture__contrat', 'facture__contrat__client']
    list_select_related = True


class PrestationAdmin(admin.ModelAdmin):
    form = forms.PrestationForm
    list_select_related = True


admin.site.register(models.Client, ClientAdmin)
admin.site.register(models.Contrat, ContratAdmin)
admin.site.register(models.Facture, FactureAdmin)
admin.site.register(models.Prestation, PrestationAdmin)

eo_gestion.admin.site.register(models.Client, ClientAdmin)
eo_gestion.admin.site.register(models.Contrat, ContratAdmin)
eo_gestion.admin.site.register(models.Facture, FactureAdmin)
eo_gestion.admin.site.register(models.Payment, PaymentAdmin)
