# -*- coding: utf-8 -*-

# passerelle.apps.family
# Copyright (C) 2016  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 json
import os
import shutil
import sys
import zipfile

from django.core.exceptions import ValidationError
from django.core.files.storage import default_storage
from django.utils.translation import ugettext_lazy as _
from django.http import Http404, HttpResponse
from django.db import models, transaction
from django.utils.timezone import make_aware, datetime, get_current_timezone, now

from passerelle.base.models import BaseResource
from passerelle.utils.api import endpoint

SEXES = (
    ('M', _('Male')),
    ('F', _('Female')),
)

DATE_FORMAT = '%Y-%m-%d'
DATETIME_FORMAT = DATE_FORMAT + 'T%H:%M:%S'

def get_datetime(date):
    if not date:
        return None
    try:
        return make_aware(datetime.strptime(date,DATETIME_FORMAT),
                          get_current_timezone())
    except ValueError:
        pass
    try:
        return datetime.strptime(date, DATE_FORMAT).date()
    except ValueError:
        pass
    return None

def format_person(p):
    data = {'id': str(p.id),
            'text': p.fullname,
            'first_name': p.first_name,
            'last_name': p.last_name,
            'birthdate': p.birthdate,
            'sex': p.sex}
    for attr in ('address', 'city', 'post_code', 'phone',
                 'cellphone', 'email'):
        if hasattr(p, attr):
            data[attr] = getattr(p, attr)
    return data

def format_invoice(i):
    invoice = {'id': i.external_id,
            'label': i.label,
            'amount': i.amount,
            'total_amount': i.total_amount,
            'created': i.issue_date,
            'pay_limit_date': i.pay_limit_date,
            'payment_date': i.payment_date,
            'paid': i.paid,
            'online_payment': i.online_payment,
            'no_online_payment_reason': i.no_online_payment_reason,
            'has_pdf': i.has_pdf}
    if now().date() > i.pay_limit_date:
        invoice['online_payment'] = False
        invoice['no_online_payment_reason'] = 'past-due-date'
    return invoice

class FileNotFoundError(Exception):
    http_status = 200
    log_error = False


def dict_cherry_pick(d1, attributes):
    d2 = {}
    for attribute in attributes:
        old_key = new_key = attribute
        if isinstance(attribute, tuple):
            old_key, new_key = attribute
        if not old_key in d1:
            continue
        d2[new_key] = d1[old_key]
    return d2

class GenericFamily(BaseResource):
    category = _('Business Process Connectors')
    archive = models.FileField(_('Data Archive'), upload_to='archives')
    file_format = models.CharField(
            _('File Format'), max_length=40,
            choices=(
                ('native', _('Native')),
                ('concerto_fondettes', _('Concerto extract from Fondettes')),
                ('concerto_orleans', _(u'Concerto extract from Orléans')),
            ),
            default='native')

    class Meta:
        verbose_name = _('Generic Family Connector')

    @classmethod
    def get_icon_class(cls):
        return 'family'

    @classmethod
    def is_enabled(cls):
        return True

    @classmethod
    def get_verbose_name(cls):
        return cls._meta.verbose_name

    def clean(self):
        if self.archive:
            try:
                archive = zipfile.ZipFile(self.archive)
            except zipfile.BadZipfile:
                raise ValidationError(_('Invalid zip file.'))
            if self.file_format != 'native':
                modname = 'family.loaders.%s' % self.file_format
                __import__(modname)
                module = sys.modules[modname]
                module.Loader(self).clean(archive)

        return super(GenericFamily, self).clean()

    @transaction.atomic
    def save(self, *args, **kwargs):
        super(GenericFamily, self).save(*args, **kwargs)
        if not zipfile.is_zipfile(self.archive.path):
            return

        invoices_dir = default_storage.path('family-%s/invoices' % self.id)
        if not os.path.exists(invoices_dir):
            os.makedirs(invoices_dir)

        archive = zipfile.ZipFile(self.archive.path)
        if self.file_format != 'native':
            modname = 'family.loaders.%s' % self.file_format
            __import__(modname)
            module = sys.modules[modname]
            module.Loader(self).load(archive)
            return

        archive_files = archive.namelist()

        family_files = [d for d in archive_files if d.endswith('.json')]
        families = []
        invoices = []
        children = []
        adults = []

        for f in family_files:
            family_data = json.loads(archive.read(f))
            families.append(family_data['id'])
            address = family_data.get('address') or {}
            family_data.update(address)
            data = dict_cherry_pick(family_data,
                    ('login',
                     'password',
                     'family_quotient',
                     ('number', 'street_number'),
                     ('postal_code', 'zipcode'),
                     ('street', 'street_name'),
                     ('complement', 'address_complement')))
            family, created = Family.objects.update_or_create(external_id=family_data['id'],
                                    resource=self, defaults=data)
            for adult in family_data.get('adults') or []:
                adults.append(adult['id'])
                adult_address = adult.get('address') or {}
                adult.update(adult_address)
                data = dict_cherry_pick(adult,
                        ('first_name',
                         'last_name',
                         'phone',
                         ('mobile', 'cellphone'),
                         'sex',
                         ('number', 'street_number'),
                         ('postal_code', 'zipcode'),
                         ('street', 'street_name'),
                         ('complement', 'address_complement'),
                         'country')
                        )
                Adult.objects.update_or_create(family=family,
                        external_id=adult['id'], defaults=data)
            # cleanup adults
            Adult.objects.exclude(external_id__in=adults).delete()

            for child in family_data.get('children') or []:
                children.append(child['id'])
                data = dict_cherry_pick(child,
                        ('first_name', 'last_name', 'sex', 'birthdate'))
                Child.objects.get_or_create(family=family,
                        external_id=child['id'], defaults=data)
            # cleanup children
            Child.objects.exclude(external_id__in=children).delete()

            for invoice in family_data['invoices']:
                invoices.append(invoice['id'])
                data = dict_cherry_pick(invoice,
                        ('label',
                         ('created', 'issue_date'),
                         'pay_limit_date',
                        'litigation_date', 'total_amount', 'payment_date',
                        'amount', 'autobilling'))
                for date_attribute in data.keys():
                    if not date_attribute.endswith('_date'):
                        continue
                    data[date_attribute] = get_datetime(data[date_attribute])
                data['paid'] = bool(data.get('payment_date'))
                Invoice.objects.update_or_create(resource=self,
                        family=family, external_id=invoice['id'], defaults=data)
                if 'invoices/%s.pdf' % invoice['id'] in archive_files:
                    with open(os.path.join(invoices_dir, '%s.pdf' % invoice['id']), 'w') as fp:
                        fp.write(archive.read('invoices/%s.pdf' % invoice['id']))

        # cleanup invoices
        Invoice.objects.exclude(external_id__in=invoices).delete()
        for filename in os.listdir(invoices_dir):
            file_invoice_id = os.path.splitext(filename)[0]
            if not file_invoice_id in invoices:
                os.unlink(os.path.join(invoices_dir, filename))

        # cleanup families
        Family.objects.exclude(external_id__in=families).delete()

    def get_family_by_nameid(self, NameID):
        try:
            link = FamilyLink.objects.get(resource=self, name_id=NameID)
            return link.family
        except FamilyLink.DoesNotExist:
            return None

    @endpoint(serializer_type='json-api', name='family', perm='can_access', pattern='^link/$')
    def family_link(self, request, NameID=None, login=None, password=None, **kwargs):
        """
        Links a NameID to a person
        """
        try:
            f = Family.objects.get(login=login, password=password, resource=self)
        except Family.DoesNotExist:
            return False
        FamilyLink.objects.get_or_create(resource=self, name_id=NameID, family=f)
        return True

    @endpoint(serializer_type='json-api', name='family', perm='can_access', pattern='^unlink/$')
    def family_unlink(self, request, NameID=None, **kwargs):
        """
        Unlinks a NameID from a person
        """
        print "Unlink"
        try:
            FamilyLink.objects.get(resource=self, name_id=NameID).delete()
            return True
        except FamilyLink.DoesNotExist:
            return False

    @endpoint(serializer_type='json-api', perm='can_access', name='family')
    def family_infos(self, request, NameID, **kwargs):
        """
        Displays the information of person's family
        """
        family = self.get_family_by_nameid(NameID)
        if not family:
            return

        data = {'id': family.get_display_id(), 'adults': [], 'children': []}
        for adult in family.adult_set.all():
            data['adults'].append(format_person(adult))
        for child in family.child_set.all():
            data['children'].append(format_person(child))
        return data

    @endpoint(serializer_type='json-api', name='family', perm='can_access', pattern='^adults/$')
    def adults_infos(self, request, NameID):
        data = self.family_infos(request, NameID)
        return data['adults']

    @endpoint(serializer_type='json-api', name='family', perm='can_access', pattern='^children/$')
    def children_infos(self, request, NameID, **kwargs):
        data = self.family_infos(request, NameID)
        return data['children']

    def get_invoices(self, NameID, paid=False):
        family = self.get_family_by_nameid(NameID)
        if not family:
            return []
        invoices = []
        for i in family.invoice_set.exclude(payment_date__isnull=paid):
            invoices.append(format_invoice(i))
        return invoices

    @endpoint(serializer_type='json-api', name='regie', pattern='^invoices/$')
    def active_invoices(self, request, NameID):
        return self.get_invoices(NameID)

    @endpoint(serializer_type='json-api', name='regie', perm='can_access', pattern='^invoices/history/$')
    def invoices_history(self, request, NameID, **kwargs):
        return self.get_invoices(NameID, paid=True)

    def get_invoice(self, invoice_id):
        try:
            return self.invoice_set.get(external_id=invoice_id)
        except Invoice.DoesNotExist:
            return None

    @endpoint(serializer_type='json-api', name='regie', perm='can_access',
              pattern='^invoice/(?P<invoice_id>\w+)/$')
    def get_invoice_details(self, request, invoice_id, NameID=None, email=None, **kwargs):
        invoice = self.get_invoice(invoice_id)
        if not invoice:
            return
        return format_invoice(invoice)

    @endpoint(serializer_type='json-api', name='regie', perm='can_access',
              pattern='^invoice/(?P<invoice_id>\w+)/pdf/$')
    def get_invoice_pdf(self, request, invoice_id, **kwargs):
        invoice = self.get_invoice(invoice_id)
        if not invoice:
            raise FileNotFoundError
        return invoice.get_pdf()

    @endpoint(serializer_type='json-api', name='regie', methods=['post'],
              perm='can_access', pattern='^invoice/(?P<invoice_id>\w+)/pay/$')
    def pay_invoice(self, request, invoice_id, **kwargs):
        data = json.loads(request.body)
        invoice = self.get_invoice(invoice_id)
        if not invoice:
            return False
        transaction_date = get_datetime(data['transaction_date'])
        invoice.paid = True
        invoice.payment_date = transaction_date
        invoice.amount = 0
        invoice.payment_transaction_id = data['transaction_id']
        invoice.save()
        return True


class FamilyLink(models.Model):
    resource = models.ForeignKey('GenericFamily')
    name_id = models.CharField(max_length=256)
    family = models.ForeignKey('Family')


class Family(models.Model):
    resource = models.ForeignKey('GenericFamily')
    external_id = models.CharField(_('External identifier'),
                max_length=16, db_index=True)
    login = models.CharField(_('Login'), max_length=64, null=True)
    password = models.CharField(_('Password'), max_length=64, null=True)
    street_number = models.CharField(_('Street number'), max_length=32, null=True)
    street_name = models.CharField(_('Street name'), max_length=128, null=True)
    address_complement = models.CharField(_('Address complement'),
                            max_length=64, null=True)
    zipcode = models.CharField(_('Zipcode'), max_length=16, null=True)
    city = models.CharField(_('City'), max_length=64, null=True)
    family_quotient = models.DecimalField(_('Family quotient'), max_digits=10,
                                    decimal_places=2, default=0)
    creation_timestamp = models.DateTimeField(auto_now_add=True)
    update_timestamp = models.DateTimeField(auto_now=True)


    def get_display_id(self):
        return self.external_id


class Person(models.Model):
    family = models.ForeignKey('Family')
    external_id = models.CharField(_('Person\'s external id'), max_length=32,
                    db_index=True)
    first_name = models.CharField(_('First name'), max_length=64)
    last_name = models.CharField(_('Last name'), max_length=64)
    sex = models.CharField(_('Sex'), max_length=1, choices=SEXES)
    birthdate = models.DateField(_('Birthdate'), null=True, blank=True)
    creation_timestamp = models.DateTimeField(auto_now_add=True)
    update_timestamp = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True

    @property
    def fullname(self):
        return u'%s %s' % (self.first_name, self.last_name)

    def __unicode__(self):
        return self.fullname


class Adult(Person):
    phone = models.CharField(_('Phone'), max_length=32, null=True)
    cellphone = models.CharField(_('Cellphone'), max_length=32, null=True)
    street_number = models.CharField(_('Street number'), max_length=32, null=True)
    street_name = models.CharField(_('Street name'), max_length=128, null=True)
    address_complement = models.CharField(_('Address complement'),
                            max_length=64, null=True)
    zipcode = models.CharField(_('Zipcode'), max_length=16, null=True)
    city = models.CharField(_('City'), max_length=64, null=True)
    country = models.CharField(_('Country'), max_length=128, null=True)
    email = models.EmailField(_('Email'), null=True)


class Child(Person):
    pass


class Invoice(models.Model):
    resource = models.ForeignKey('GenericFamily')
    family = models.ForeignKey('Family', null=True)
    external_id = models.CharField(_('External id'), max_length=128, db_index=True)
    label = models.CharField(_('Label'), max_length=128, null=True)
    issue_date = models.DateField(_('Issue date'), null=True)
    pay_limit_date = models.DateField(_('Due date'), null=True)
    litigation_date = models.DateField(_('Litigation date'), null=True)
    total_amount = models.DecimalField(_('Total amount'), max_digits=6,
                        decimal_places=2, default=0)
    amount = models.DecimalField(_('Amount'), max_digits=6, decimal_places=2, default=0)
    payment_date = models.DateTimeField(_('Payment date'), null=True)
    paid = models.BooleanField(_('Paid'), default=False)
    autobilling = models.BooleanField(_('Autobilling'), default=False)
    online_payment = models.BooleanField(_('Online payment'), default=True)
    no_online_payment_reason = models.CharField(_('No online payment reason'),
            max_length=100, null=True)
    payment_transaction_id = models.CharField(_('Payment transaction id'),
                                max_length=128, null=True)
    creation_timestamp = models.DateTimeField(auto_now_add=True)
    update_timestamp = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['issue_date']

    def pdf_filename(self):
        invoices_dir = default_storage.path('family-%s/invoices' % self.resource.id)
        return os.path.join(invoices_dir, '%s.pdf' % self.external_id)

    @property
    def has_pdf(self):
        return os.path.exists(self.pdf_filename())

    def get_pdf(self):
        if not self.has_pdf:
            raise Http404(_('PDF file not found'))

        response = HttpResponse(open(self.pdf_filename()).read(), content_type='application/pdf')
        response['Content-Disposition'] = 'attachment; filename=%s.pdf' % self.external_id
        return response

    def write_pdf(self, contents):
        with open(self.pdf_filename(), 'w') as fp:
            fp.write(contents)
