import datetime
from dateutil.relativedelta import relativedelta

from django.core.validators import ValidationError, RegexValidator
from django.db import models
from django.db.models import F, Sum
from django.db.models.functions import Coalesce
from django.template import loader, Context
from django.utils import timezone, translation
from django.utils.encoding import force_text
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _

from orchestra.contrib.accounts.models import Account
from orchestra.contrib.contacts.models import Contact
from orchestra.core import validators
from orchestra.utils.html import html_to_pdf

from . import settings


class BillContact(models.Model):
    account = models.OneToOneField('accounts.Account', verbose_name=_("account"),
        related_name='billcontact')
    name = models.CharField(_("name"), max_length=256, blank=True,
        help_text=_("Account full name will be used when left blank."))
    address = models.TextField(_("address"))
    city = models.CharField(_("city"), max_length=128,
        default=settings.BILLS_CONTACT_DEFAULT_CITY)
    zipcode = models.CharField(_("zip code"), max_length=10,
        validators=[RegexValidator(r'^[0-9A-Z]{3,10}$', _("Enter a valid zipcode."))])
    country = models.CharField(_("country"), max_length=20,
        choices=settings.BILLS_CONTACT_COUNTRIES,
        default=settings.BILLS_CONTACT_DEFAULT_COUNTRY)
    vat = models.CharField(_("VAT number"), max_length=64)
    
    def __str__(self):
        return self.name
    
    def get_name(self):
        return self.name or self.account.get_full_name()
    
    def clean(self):
        self.vat = self.vat.strip()
        self.city = self.city.strip()
        validators.all_valid({
            'vat': (validators.validate_vat, self.vat, self.country),
            'zipcode': (validators.validate_zipcode, self.zipcode, self.country)
        })


class BillManager(models.Manager):
    def get_queryset(self):
        queryset = super(BillManager, self).get_queryset()
        if self.model != Bill:
            bill_type = self.model.get_class_type()
            queryset = queryset.filter(type=bill_type)
        return queryset


class Bill(models.Model):
    OPEN = ''
    PAID = 'PAID'
    PENDING = 'PENDING'
    BAD_DEBT = 'BAD_DEBT'
    PAYMENT_STATES = (
        (PAID, _("Paid")),
        (PENDING, _("Pending")),
        (BAD_DEBT, _("Bad debt")),
    )
    BILL = 'BILL'
    INVOICE = 'INVOICE'
    AMENDMENTINVOICE = 'AMENDMENTINVOICE'
    FEE = 'FEE'
    AMENDMENTFEE = 'AMENDMENTFEE'
    PROFORMA = 'PROFORMA'
    TYPES = (
        (INVOICE, _("Invoice")),
        (AMENDMENTINVOICE, _("Amendment invoice")),
        (FEE, _("Fee")),
        (AMENDMENTFEE, _("Amendment Fee")),
        (PROFORMA, _("Pro forma")),
    )
    
    number = models.CharField(_("number"), max_length=16, unique=True, blank=True)
    account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
         related_name='%(class)s')
    type = models.CharField(_("type"), max_length=16, choices=TYPES)
    created_on = models.DateField(_("created on"), auto_now_add=True)
    closed_on = models.DateField(_("closed on"), blank=True, null=True)
    is_open = models.BooleanField(_("open"), default=True)
    is_sent = models.BooleanField(_("sent"), default=False)
    due_on = models.DateField(_("due on"), null=True, blank=True)
    updated_on = models.DateField(_("updated on"), auto_now=True)
    # TODO allways compute total or what?
    total = models.DecimalField(max_digits=12, decimal_places=2, null=True)
    comments = models.TextField(_("comments"), blank=True)
    html = models.TextField(_("HTML"), blank=True)
    
    objects = BillManager()
    
    class Meta:
        get_latest_by = 'id'
    
    def __str__(self):
        return self.number
    
    @classmethod
    def get_class_type(cls):
        return cls.__name__.upper()
    
    @cached_property
    def seller(self):
        return Account.get_main().billcontact
    
    @cached_property
    def buyer(self):
        return self.account.billcontact
    
    @property
    def has_multiple_pages(self):
        return self.type != self.FEE
    
    @cached_property
    def payment_state(self):
        if self.is_open or self.get_type() == self.PROFORMA:
            return self.OPEN
        secured = self.transactions.secured().amount() or 0
        if abs(secured) >= abs(self.get_total()):
            return self.PAID
        elif self.transactions.exclude_rejected().exists():
            return self.PENDING
        return self.BAD_DEBT
    
    def get_total(self):
        if not self.is_open:
            return self.total
        try:
            return self.computed_total
        except AttributeError:
            self.computed_total = self.compute_total()
            return self.computed_total
    
    def get_payment_state_display(self):
        value = self.payment_state
        return force_text(dict(self.PAYMENT_STATES).get(value, value))
    
    def get_current_transaction(self):
        return self.transactions.exclude_rejected().first()
    
    def get_type(self):
        return self.type or self.get_class_type()
    
    def get_number(self):
        cls = type(self)
        bill_type = self.get_type()
        if bill_type == self.BILL:
            raise TypeError('This method can not be used on BILL instances')
        bill_type = bill_type.replace('AMENDMENT', 'AMENDMENT_')
        prefix = getattr(settings, 'BILLS_%s_NUMBER_PREFIX' % bill_type)
        if self.is_open:
            prefix = 'O{}'.format(prefix)
        bills = cls.objects.filter(number__regex=r'^%s[1-9]+' % prefix)
        last_number = bills.order_by('-number').values_list('number', flat=True).first()
        if last_number is None:
            last_number = 0
        else:
            last_number = int(last_number[len(prefix)+4:])
        number = last_number + 1
        year = timezone.now().strftime("%Y")
        number_length = settings.BILLS_NUMBER_LENGTH
        zeros = (number_length - len(str(number))) * '0'
        number = zeros + str(number)
        return '{prefix}{year}{number}'.format(prefix=prefix, year=year, number=number)
    
    def get_due_date(self, payment=None):
        now = timezone.now()
        if payment:
            return now + payment.get_due_delta()
        return now + relativedelta(months=1)
    
    def close(self, payment=False):
        if not self.is_open:
            raise TypeError("Bill not in Open state.")
        if payment is False:
            payment = self.account.paymentsources.get_default()
        if not self.due_on:
            self.due_on = self.get_due_date(payment=payment)
        self.total = self.compute_total()
        transaction = None
        if self.get_type() != self.PROFORMA:
            transaction = self.transactions.create(bill=self, source=payment, amount=self.total)
        self.closed_on = timezone.now()
        self.is_open = False
        self.is_sent = False
        self.number = self.get_number()
        self.html = self.render(payment=payment)
        self.save()
        return transaction
    
    def send(self):
        html = self.html or self.render()
        pdf = html_to_pdf(html, pagination=self.has_multiple_pages)
        self.account.send_email(
            template=settings.BILLS_EMAIL_NOTIFICATION_TEMPLATE,
            context={
                'bill': self,
                'settings': settings,
            },
            email_from=settings.BILLS_SELLER_EMAIL,
            contacts=(Contact.BILLING,),
            attachments=[
                ('%s.pdf' % self.number, pdf, 'application/pdf')
            ]
        )
        self.is_sent = True
        self.save(update_fields=['is_sent'])
    
    def render(self, payment=False, language=None):
        with translation.override(language or self.account.language):
            if payment is False:
                payment = self.account.paymentsources.get_default()
            context = Context({
                'bill': self,
                'lines': self.lines.all().prefetch_related('sublines'),
                'seller': self.seller,
                'buyer': self.buyer,
                'seller_info': {
                    'phone': settings.BILLS_SELLER_PHONE,
                    'website': settings.BILLS_SELLER_WEBSITE,
                    'email': settings.BILLS_SELLER_EMAIL,
                    'bank_account': settings.BILLS_SELLER_BANK_ACCOUNT,
                },
                'currency': settings.BILLS_CURRENCY,
                'payment': payment and payment.get_bill_context(),
                'default_due_date': self.get_due_date(payment=payment),
                'now': timezone.now(),
            })
            template_name = 'BILLS_%s_TEMPLATE' % self.get_type()
            template = getattr(settings, template_name, settings.BILLS_DEFAULT_TEMPLATE)
            bill_template = loader.get_template(template)
            html = bill_template.render(context)
            html = html.replace('-pageskip-', '<pdf:nextpage />')
        return html
    
    def save(self, *args, **kwargs):
        if not self.type:
            self.type = self.get_type()
        if not self.number:
            self.number = self.get_number()
        super(Bill, self).save(*args, **kwargs)
    
    def compute_subtotals(self):
        subtotals = {}
        lines = self.lines.annotate(totals=(F('subtotal') + Coalesce(F('sublines__total'), 0)))
        for tax, total in lines.values_list('tax', 'totals'):
            subtotal, taxes = subtotals.get(tax) or (0, 0)
            subtotal += total
            subtotals[tax] = (subtotal, round(tax/100*subtotal, 2))
        return subtotals
    
    def compute_total(self):
        totals = self.lines.annotate(
            totals=(F('subtotal') + Coalesce(F('sublines__total'), 0)) * (1+F('tax')/100))
        return round(totals.aggregate(Sum('totals'))['totals__sum'], 2)


class Invoice(Bill):
    class Meta:
        proxy = True


class AmendmentInvoice(Bill):
    class Meta:
        proxy = True


class Fee(Bill):
    class Meta:
        proxy = True


class AmendmentFee(Bill):
    class Meta:
        proxy = True


class ProForma(Bill):
    class Meta:
        proxy = True


class BillLine(models.Model):
    """ Base model for bill item representation """
    bill = models.ForeignKey(Bill, verbose_name=_("bill"), related_name='lines')
    description = models.CharField(_("description"), max_length=256)
    rate = models.DecimalField(_("rate"), blank=True, null=True, max_digits=12, decimal_places=2)
    quantity = models.DecimalField(_("quantity"), max_digits=12, decimal_places=2)
    verbose_quantity = models.CharField(_("Verbose quantity"), max_length=16)
    subtotal = models.DecimalField(_("subtotal"), max_digits=12, decimal_places=2)
    tax = models.DecimalField(_("tax"), max_digits=4, decimal_places=2)
    start_on = models.DateField(_("start"))
    end_on = models.DateField(_("end"), null=True)
    order = models.ForeignKey(settings.BILLS_ORDER_MODEL, null=True, blank=True,
        help_text=_("Informative link back to the order"), on_delete=models.SET_NULL)
    order_billed_on = models.DateField(_("order billed"), null=True, blank=True)
    order_billed_until = models.DateField(_("order billed until"), null=True, blank=True)
    created_on = models.DateField(_("created"), auto_now_add=True)
    # Amendment
    amended_line = models.ForeignKey('self', verbose_name=_("amended line"),
        related_name='amendment_lines', null=True, blank=True)
    
    def __str__(self):
        return "#%i" % self.pk
    
    def compute_total(self):
        """ Computes subline discounts """
        if self.pk:
            return self.subtotal + sum([sub.total for sub in self.sublines.all()])
    
    def get_verbose_quantity(self):
        return self.verbose_quantity or self.quantity
    
    def get_verbose_period(self):
        from django.template.defaultfilters import date
        date_format = "N 'y"
        if self.start_on.day != 1 or self.end_on.day != 1:
            date_format = "N j, 'y"
            end = date(self.end_on, date_format)
        else:
            end = date((self.end_on - datetime.timedelta(days=1)), date_format)
        ini = date(self.start_on, date_format).capitalize()
        end = end.capitalize()
        if not self.end_on:
            return ini
        if ini == end:
            return ini
        return "{ini} / {end}".format(ini=ini, end=end)
    
#    def save(self, *args, **kwargs):
#        super(BillLine, self).save(*args, **kwargs)
#        if self.bill.is_open:
#            self.bill.total = self.bill.get_total()
#            self.bill.save(update_fields=['total'])


class BillSubline(models.Model):
    """ Subline used for describing an item discount """
    VOLUME = 'VOLUME'
    COMPENSATION = 'COMPENSATION'
    OTHER = 'OTHER'
    TYPES = (
        (VOLUME, _("Volume")),
        (COMPENSATION, _("Compensation")),
        (OTHER, _("Other")),
    )
    
    # TODO: order info for undoing
    line = models.ForeignKey(BillLine, verbose_name=_("bill line"), related_name='sublines')
    description = models.CharField(_("description"), max_length=256)
    total = models.DecimalField(max_digits=12, decimal_places=2)
    type = models.CharField(_("type"), max_length=16, choices=TYPES, default=OTHER)
    
#    def save(self, *args, **kwargs):
#        # TODO cost of this shit
#        super(BillSubline, self).save(*args, **kwargs)
#        if self.line.bill.is_open:
#            self.line.bill.total = self.line.bill.get_total()
#            self.line.bill.save(update_fields=['total'])
