# 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/>.


from collections import defaultdict
from datetime import date, datetime, timedelta
from decimal import Decimal, InvalidOperation

from django import template
from django.db import transaction
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from django.urls import reverse
from django.utils.formats import number_format
from django.utils.six import text_type
from django.utils.timesince import timesince

from eo_gestion.eo_banque.models import LigneBanquePop
from eo_gestion.eo_facture.models import DELAI_PAIEMENT, Contrat, Facture, Payment
from eo_gestion.utils import percentage

from ...decorators import cache

register = template.Library()


@register.inclusion_tag('eo_facture/impayee.html')
@cache
def impayees():
    factures_impayees = [
        f
        for f in Facture.objects.select_related('client', 'contrat', 'contrat__client')
        .prefetch_related('lignes', 'payments')
        .avec_solde()
        .filter(paid=False)
        .order_by('emission')
        if f.solde() < 0
    ]
    factures = []
    soldes = [-f.solde() for f in factures_impayees]
    soldes_ht = [
        f.montant * (solde / f.montant_ttc) if f.montant_ttc else Decimal(0)
        for solde, f in zip(soldes, factures_impayees)
    ]
    solde = sum(soldes)
    solde_ht = sum(soldes_ht)
    today = date.today()
    after = datetime.now() - timedelta(days=365)
    after = after.date()
    lignes = LigneBanquePop.objects.filter(date_operation__gte=after, montant__in=soldes, payments=None)
    for f in factures_impayees:
        vieille = False
        if (today - f.emission) > timedelta(days=DELAI_PAIEMENT):
            vieille = True
        encaissements = [ligne for ligne in lignes if ligne.montant == f.montant_ttc]
        factures.append({'facture': f, 'encaissements': encaissements, 'vieille': vieille})
    return {'montant': solde, 'montant_ht': solde_ht, 'factures': factures}


@register.inclusion_tag('eo_facture/income.html')
@cache
def income():
    def AmountByYear():
        return defaultdict(lambda: Decimal(0))

    invoices = (
        Facture.objects.filter(proforma=False)
        .select_related('client', 'contrat', 'contrat__client')
        .prefetch_related('lignes', 'payments')
    )
    paid_by_year = AmountByYear()
    today_year = date.today().year
    sous_traite_by_year = AmountByYear()
    for invoice in invoices:
        if invoice.paid:
            paid_by_year[invoice.accounting_year] += invoice.montant
        else:
            tva = invoice.tva
            ht = invoice.montant
            try:
                taux_moyen = tva / ht
            except (ZeroDivisionError, InvalidOperation):
                taux_moyen = Decimal(0)
            paid_by_year[invoice.accounting_year] += (
                (invoice.montant_ttc + invoice.solde()) * Decimal(1) / (Decimal(1) + taux_moyen)
            )
    invoiced_by_year = defaultdict(lambda: Decimal(0))
    invoiced_by_year_and_client = defaultdict(lambda: defaultdict(lambda: Decimal(0)))
    for invoice in invoices:
        montant = invoice.montant - invoice.sous_traite
        invoiced_by_year[invoice.accounting_year] += montant
        sous_traite_by_year[invoice.accounting_year] += invoice.sous_traite
        invoiced_by_year_and_client[invoice.accounting_year][invoice.client.id] += montant
    invoiced_clients_by_year = {}
    for year in invoiced_by_year_and_client:
        clients = list(
            sorted(
                invoiced_by_year_and_client[year].keys(),
                key=lambda x, year=year: -invoiced_by_year_and_client[year][x],
            )
        )
        invoiced_clients_by_year[year] = clients
    contracted_by_year = defaultdict(lambda: Decimal(0))
    for contract in Contrat.objects.all().prefetch_related('factures', 'factures__lignes', 'prestations'):
        adjust = 0
        for year, fraction in contract.percentage_per_year:
            amount = contract.montant() * fraction
            if year > today_year:
                contracted_by_year[year] += amount
            else:
                adjust += amount
        for f in contract.factures.all():
            if f.proforma:
                continue
            if f.accounting_year <= today_year:
                adjust -= f.montant - f.sous_traite
        if adjust > 0:
            contracted_by_year[today_year] += adjust
    this_year = date.today().year
    income_by_year = []
    for year in invoiced_by_year:
        invoiced_by_year[year] -= paid_by_year[year]
    for year in range(this_year - 1, this_year + 2):
        income_by_year.append(
            {
                'year': year,
                'paid': paid_by_year[year],
                'invoiced': invoiced_by_year[year],
                'contracted': contracted_by_year[year],
                'sous_traite': sous_traite_by_year[year],
                'total': paid_by_year[year] + invoiced_by_year[year] + contracted_by_year[year],
            }
        )
    return {'income_by_year': income_by_year}


def zero_dict():
    return defaultdict(lambda: Decimal(0))


class StringWithHref(text_type):
    def __new__(cls, v, href=None):
        return text_type.__new__(cls, v)

    def __init__(self, v, href=None):
        super().__init__()
        self.href = href


def client_and_link(c):
    s = text_type(c)
    url = reverse("admin:eo_facture_client_change", args=(c.id,))
    return StringWithHref(s, url)


def dict_of_list():
    return defaultdict(lambda: [])


@register.inclusion_tag("eo_facture/table.html")
@cache
def income_by_clients(year=None):
    if not year:
        year = date.today().year
    contracted_by_clients = zero_dict()
    invoiced_by_clients = zero_dict()
    total_by_clients = zero_dict()
    contracts_by_clients = dict_of_list()
    invoices_by_clients = dict_of_list()
    for contrat in Contrat.objects.select_related('client').prefetch_related(
        'factures', 'factures__lignes', 'prestations'
    ):
        # how much as already been invoiced before previsions
        adjust = 0
        invoiced = 0
        for percent_year, fraction in contrat.percentage_per_year:
            if percent_year > year:
                continue
            adjust += contrat.montant() * fraction
        for facture in contrat.factures.all():
            if facture.proforma:
                continue
            if facture.accounting_year <= year:
                adjust -= facture.montant
            if facture.accounting_year == year:
                invoiced += facture.montant
        if adjust > 0:
            contracted_by_clients[contrat.client] += adjust
            total_by_clients[contrat.client] += adjust
        if invoiced:
            invoiced_by_clients[contrat.client] += invoiced
            total_by_clients[contrat.client] += invoiced
    for facture in (
        Facture.objects.select_related('client')
        .filter(contrat__isnull=True)
        .for_year(year)
        .prefetch_related('lignes')
    ):
        if facture.proforma:
            continue
        invoiced_by_clients[facture.client] += facture.montant
        total_by_clients[facture.client] += facture.montant
    total = sum(total_by_clients.values())
    total_invoiced = sum(invoiced_by_clients.values())
    total_contracted = sum(contracted_by_clients.values())
    percent_by_clients = {i: Decimal(100) * v / total for i, v in total_by_clients.items()}
    clients = sorted(total_by_clients.keys(), key=lambda x: -total_by_clients[x])
    running_total = Decimal(0)
    # compute pareto index
    pareto = {}
    for client in clients:
        running_total += percent_by_clients[client]
        pareto[client] = running_total
    return dict(
        title="Chiffre d'affaire prévisionnel par client pour %s" % year,
        name="previsional-income-by-client-%s" % year,
        headers=[
            ('client', 'Client'),
            ('income', 'Chiffre d\'affaire'),
            ('invoiced', 'Facturé'),
            ('percent_invoiced', 'Pourcentage facturé'),
            ('contracted', 'Contracté'),
            ('percent_contracted', 'Pourcentage contracté'),
            ('percent', 'Pourcentage'),
            ('pareto', 'Pareto'),
        ],
        contracts_by_clients=list(contracts_by_clients.items()),
        invoices_by_clients=list(invoices_by_clients.items()),
        table=[
            (
                client_and_link(c),
                total_by_clients[c],
                invoiced_by_clients[c],
                percentage(invoiced_by_clients[c], total_by_clients[c]),
                contracted_by_clients[c],
                percentage(contracted_by_clients[c], total_by_clients[c]),
                percent_by_clients[c],
                pareto[c],
            )
            for c in clients
        ]
        + [
            (
                'Total',
                total,
                total_invoiced,
                percentage(total_invoiced, total),
                total_contracted,
                percentage(total_contracted, total),
                Decimal(100),
                '',
            )
        ],
    )


@register.inclusion_tag('eo_facture/a_facturer.html')
@cache
def a_facturer():
    contrats_a_facturer = []
    for contrat in (
        Contrat.objects.all()
        .select_related('client')
        .prefetch_related('prestations', 'factures', 'factures__lignes')
    ):
        facture = contrat.montant_facture()
        a_facture = contrat.montant()
        if a_facture and facture < a_facture:
            if contrat.factures.count() > 0:
                depuis = max(facture.emission for facture in contrat.factures.all())
            else:
                depuis = contrat.creation
            depuis = (date.today() - depuis).days
            contrats_a_facturer.append(
                {
                    'contrat': contrat,
                    'pourcentage': (facture / a_facture) * Decimal(100),
                    'montant': a_facture - facture,
                    'depuis': depuis,
                }
            )
    contrats_a_facturer.sort(key=lambda x: -x['depuis'])
    montant = sum(x['montant'] for x in contrats_a_facturer)
    return {'a_facturer': contrats_a_facturer, 'montant': montant}


@register.filter(name='ago')
def ago(date):
    ago = timesince(date)
    # selects only the first part of the returned string
    return ago.split(",")[0]


@register.filter(is_safe=True)
def amountformat(value, use_l10n=True):
    return number_format(Decimal(value).quantize(Decimal("0.01")), force_grouping=True)


# invalidate impayees() cache when Payment set changes
@receiver(post_save, sender=Payment)
def payment_post_save(raw, **kwargs):
    if raw:
        return
    transaction.on_commit(impayees.recompute)


@receiver(post_delete, sender=Payment)
def payment_post_delete(**kwargs):
    transaction.on_commit(impayees.recompute)
