import logging
import itertools
import datetime
from math import ceil
from calendar import monthrange
from decimal import Decimal
from functools import reduce

from django.db import models
from django.db.models import Q
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
from django.core.validators import MinValueValidator
from django.utils import timezone
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.conf import settings

import uncloud_pay.stripe
from uncloud import AMOUNT_DECIMALS, AMOUNT_MAX_DIGITS
from uncloud.models import UncloudAddress

# Used to generate bill due dates.
BILL_PAYMENT_DELAY=datetime.timedelta(days=10)

# Initialize logger.
logger = logging.getLogger(__name__)

def start_of_month(a_day):
    """ Returns first of the month of a given datetime object"""
    return a_day.replace(day=1,hour=0,minute=0,second=0, microsecond=0)

def end_of_month(a_day):
    """ Returns first of the month of a given datetime object"""

    _, last_day = monthrange(a_day.year, a_day.month)
    return a_day.replace(day=last_day,hour=23,minute=59,second=59, microsecond=0)

def start_of_this_month():
    """ Returns first of this month"""
    a_day = timezone.now()
    return a_day.replace(day=1,hour=0,minute=0,second=0, microsecond=0)

def end_of_this_month():
    """ Returns first of this month"""
    a_day = timezone.now()

    _, last_day = monthrange(a_day.year, a_day.month)
    return a_day.replace(day=last_day,hour=23,minute=59,second=59, microsecond=0)

def end_before(a_date):
    """ Return suitable datetimefield for ending just before a_date """
    return a_date - datetime.timedelta(seconds=1)

def start_after(a_date):
    """ Return suitable datetimefield for starting just after a_date """
    return a_date + datetime.timedelta(seconds=1)

def default_payment_delay():
    return timezone.now() + BILL_PAYMENT_DELAY

class Currency(models.TextChoices):
    """
    Possible currencies to be billed
    """
    CHF   = 'CHF', _('Swiss Franc')
    EUR   = 'EUR', _('Euro')
    USD   = 'USD', _('US Dollar')


def get_balance_for_user(user):
    bills = reduce(
            lambda acc, entry: acc + entry.total,
            Bill.objects.filter(owner=user),
            0)
    payments = reduce(
            lambda acc, entry: acc + entry.amount,
            Payment.objects.filter(owner=user),
            0)
    return payments - bills

###
# Stripe

class StripeCustomer(models.Model):
    owner = models.OneToOneField( get_user_model(),
            primary_key=True,
            on_delete=models.CASCADE)
    stripe_id = models.CharField(max_length=32)

    def __str__(self):
        return self.owner.username

###
# Payments and Payment Methods.

class Payment(models.Model):
    owner = models.ForeignKey(get_user_model(),
            on_delete=models.CASCADE)

    amount = models.DecimalField(
            default=0.0,
            max_digits=AMOUNT_MAX_DIGITS,
            decimal_places=AMOUNT_DECIMALS,
            validators=[MinValueValidator(0)])

    source = models.CharField(max_length=256,
                              choices = (
                                  ('wire', 'Wire Transfer'),
                                  ('stripe', 'Stripe'),
                                  ('voucher', 'Voucher'),
                                  ('referral', 'Referral'),
                                  ('unknown', 'Unknown')
                              ),
                              default='unknown')
    timestamp = models.DateTimeField(editable=False, auto_now_add=True)

    # We override save() in order to active products awaiting payment.
    def save(self, *args, **kwargs):
        # _state.adding is switched to false after super(...) call.
        being_created = self._state.adding

        unpaid_bills_before_payment = Bill.get_unpaid_for(self.owner)
        super(Payment, self).save(*args, **kwargs) # Save payment in DB.
        unpaid_bills_after_payment = Bill.get_unpaid_for(self.owner)

        newly_paid_bills = list(
                set(unpaid_bills_before_payment) - set(unpaid_bills_after_payment))
        for bill in newly_paid_bills:
            bill.activate_products()


class PaymentMethod(models.Model):
    owner = models.ForeignKey(get_user_model(),
            on_delete=models.CASCADE,
            editable=False)
    source = models.CharField(max_length=256,
            choices = (
                ('stripe', 'Stripe'),
                ('unknown', 'Unknown'),
                ),
            default='stripe')
    description = models.TextField()
    primary = models.BooleanField(default=False, editable=False)

    # Only used for "Stripe" source
    stripe_payment_method_id = models.CharField(max_length=32, blank=True, null=True)
    stripe_setup_intent_id = models.CharField(max_length=32, blank=True, null=True)

    @property
    def stripe_card_last4(self):
        if self.source == 'stripe' and self.active:
            payment_method = uncloud_pay.stripe.get_payment_method(
                    self.stripe_payment_method_id)
            return payment_method.card.last4
        else:
            return None

    @property
    def active(self):
        if self.source == 'stripe' and self.stripe_payment_method_id != None:
            return True
        else:
            return False

    def charge(self, amount):
        if not self.active:
            raise Exception('This payment method is inactive.')

        if amount < 0: # Make sure we don't charge negative amount by errors...
            raise Exception('Cannot charge negative amount.')

        if self.source == 'stripe':
            stripe_customer = StripeCustomer.objects.get(owner=self.owner).stripe_id
            stripe_payment = uncloud_pay.stripe.charge_customer(
                    amount, stripe_customer, self.stripe_payment_method_id)
            if 'paid' in stripe_payment and stripe_payment['paid'] == False:
                raise Exception(stripe_payment['error'])
            else:
                payment = Payment.objects.create(
                        owner=self.owner, source=self.source, amount=amount)

                return payment
        else:
            raise Exception('This payment method is unsupported/cannot be charged.')

    def set_as_primary_for(self, user):
        methods = PaymentMethod.objects.filter(owner=user, primary=True)
        for method in methods:
            print(method)
            method.primary = False
            method.save()

        self.primary = True
        self.save()

    def get_primary_for(user):
        methods = PaymentMethod.objects.filter(owner=user)
        for method in methods:
            # Do we want to do something with non-primary method?
            if method.active and method.primary:
                return method

        return None

    class Meta:
        # TODO: limit to one primary method per user.
        # unique_together is no good since it won't allow more than one
        # non-primary method.
        pass

# See https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices-enum-types
class RecurringPeriodDefaultChoices(models.IntegerChoices):
    """
    This is an old class and being superseeded by the database model below
    """
    PER_365D   = 365*24*3600, _('Per 365 days')
    PER_30D    = 30*24*3600, _('Per 30 days')
    PER_WEEK   = 7*24*3600, _('Per Week')
    PER_DAY    = 24*3600, _('Per Day')
    PER_HOUR   = 3600, _('Per Hour')
    PER_MINUTE = 60, _('Per Minute')
    PER_SECOND = 1, _('Per Second')
    ONE_TIME   = 0, _('Onetime')

# RecurringPeriods
class RecurringPeriod(models.Model):
    """
    Available recurring periods.
    By default seeded from RecurringPeriodChoices
    """

    name = models.CharField(max_length=100, unique=True)
    duration_seconds = models.IntegerField(unique=True)

    @classmethod
    def populate_db_defaults(cls):
        for (seconds, name) in RecurringPeriodDefaultChoices.choices:
            obj, created = cls.objects.get_or_create(name=name,
                                                     defaults={ 'duration_seconds': seconds })

    @staticmethod
    def secs_to_name(secs):
        name = ""
        days = 0
        hours = 0

        if secs > 24*3600:
            days = secs // (24*3600)
            secs -= (days*24*3600)

        if secs > 3600:
            hours = secs // 3600
            secs -= hours*3600

        return f"{days} days {hours} hours {secs} seconds"

    def __str__(self):
        duration = self.secs_to_name(self.duration_seconds)

        return f"{self.name} ({duration})"


###
# Bills.

class BillingAddress(UncloudAddress):
    owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
    vat_number = models.CharField(max_length=100, default="", blank=True)
    active = models.BooleanField(default=False)

    class Meta:
        constraints = [
            models.UniqueConstraint(fields=['owner'],
                                    condition=Q(active=True),
                                    name='one_active_billing_address_per_user')
        ]

    @classmethod
    def populate_db_defaults(cls):
        """
        Ensure we have at least one billing address that is associated with the uncloud-admin.

        This way we are sure that an UncloudProvider can be created.

        Cannot use get_or_create as that looks for exactly one.

        """

        owner = get_user_model().objects.get(username=settings.UNCLOUD_ADMIN_NAME)
        billing_address = cls.objects.filter(owner=owner).first()

        if not billing_address:
            billing_address = cls.objects.create(owner=owner,
                                                 organization="uncloud admins",
                                                 name="Uncloud Admin",
                                                 street="Uncloudstreet. 42",
                                                 city="Luchsingen",
                                                 postal_code="8775",
                                                 country="CH",
                                                 active=True)


    @staticmethod
    def get_address_for(user):
        return BillingAddress.objects.get(owner=user, active=True)

    def __str__(self):
        return "{} - {}, {}, {} {}, {}".format(
            self.owner,
            self.full_name, self.street, self.postal_code, self.city,
            self.country)

###
# VAT

class VATRate(models.Model):
    starting_date = models.DateField(blank=True, null=True)
    ending_date = models.DateField(blank=True, null=True)
    territory_codes = models.TextField(blank=True, default='')
    currency_code = models.CharField(max_length=10)
    rate = models.FloatField()
    rate_type = models.TextField(blank=True, default='')
    description = models.TextField(blank=True, default='')

    @staticmethod
    def get_for_country(country_code):
        vat_rate = None
        try:
            vat_rate = VATRate.objects.get(
                territory_codes=country_code, start_date__isnull=False, stop_date=None
            )
            return vat_rate.rate
        except VATRate.DoesNotExist as dne:
            logger.debug(str(dne))
            logger.debug("Did not find VAT rate for %s, returning 0" % country_code)
            return 0


    def __str__(self):
        return f"{self.territory_codes}: {self.starting_date} - {self.ending_date}: {self.rate_type}"

###
# Products

class Product(models.Model):
    """
    A product is something a user can order. To record the pricing, we
    create order that define a state in time.

    A product can have *one* one_time_order and/or *one*
    recurring_order.

    If either of them needs to be updated, a new order of the same
    type will be created and links to the previous order.

    """

    name = models.CharField(max_length=256, unique=True)
    description = models.CharField(max_length=1024)
    config = models.JSONField()
    recurring_periods = models.ManyToManyField(RecurringPeriod, through='ProductToRecurringPeriod')
    currency = models.CharField(max_length=32, choices=Currency.choices, default=Currency.CHF)

    @property
    def default_recurring_period(self):
        """
        Return the default recurring Period
        """
        return self.recurring_periods.get(producttorecurringperiod__is_default=True)

    @classmethod
    def populate_db_defaults(cls):
        recurring_period = RecurringPeriod.objects.get(name="Per 30 days")

        obj, created = cls.objects.get_or_create(name="Dual Stack Virtual Machine v1",
                                                 description="A standard virtual machine",
                                                 currency=Currency.CHF,
                                                 config={
                                                     'features': {
                                                         'cores':
                                                         { 'min': 1,
                                                           'max': 48,
                                                           'one_time_price_per_unit': 0,
                                                           'recurring_price_per_unit': 3
                                                          },
                                                         'ram_gb':
                                                         { 'min': 1,
                                                           'max': 256,
                                                           'one_time_price_per_unit': 0,
                                                           'recurring_price_per_unit': 4
                                                          },
                                                         'ssd_gb':
                                                         { 'min': 10,
                                                           'one_time_price_per_unit': 0,
                                                           'recurring_price_per_unit': 0.35
                                                          },
                                                         'hdd_gb':
                                                         { 'min': 0,
                                                           'one_time_price_per_unit': 0,
                                                           'recurring_price_per_unit': 15/1000
                                                          },
                                                         'additional_ipv4_address':
                                                         { 'min': 0,
                                                           'one_time_price_per_unit': 0,
                                                           'recurring_price_per_unit': 8
                                                          },
                                                     }
                                                 }
                                                 )

        obj.recurring_periods.add(recurring_period, through_defaults= { 'is_default': True })

        obj, created = cls.objects.get_or_create(name="Dual Stack Virtual Machine v2",
                                                 description="A standard virtual machine",
                                                 currency=Currency.CHF,
                                                 config={
                                                     'features': {
                                                         'base':
                                                         { 'min': 1,
                                                           'max': 1,
                                                           'one_time_price_per_unit': 0,
                                                           'recurring_price_per_unit': 1
                                                          },
                                                         'cores':
                                                         { 'min': 1,
                                                           'max': 48,
                                                           'one_time_price_per_unit': 0,
                                                           'recurring_price_per_unit': 3
                                                          },
                                                         'ram_gb':
                                                         { 'min': 1,
                                                           'max': 256,
                                                           'one_time_price_per_unit': 0,
                                                           'recurring_price_per_unit': 4
                                                          },
                                                         'ssd_gb':
                                                         { 'min': 10,
                                                           'one_time_price_per_unit': 0,
                                                           'recurring_price_per_unit': 0.35
                                                          },
                                                         'hdd_gb':
                                                         { 'min': 0,
                                                           'one_time_price_per_unit': 0,
                                                           'recurring_price_per_unit': 15/1000
                                                          },
                                                         'additional_ipv4_address':
                                                         { 'min': 0,
                                                           'one_time_price_per_unit': 0,
                                                           'recurring_price_per_unit': 9
                                                          },
                                                     }
                                                 }
                                                 )

        obj.recurring_periods.add(recurring_period, through_defaults= { 'is_default': True })

        obj, created = cls.objects.get_or_create(name="reverse DNS",
                                                 description="Reverse DNS network",
                                                 currency=Currency.CHF,
                                                 config={
                                                     'parameters': [
                                                         'network'
                                                     ]
                                                 })
        obj.recurring_periods.add(recurring_period, through_defaults= { 'is_default': True })


    def __str__(self):
        return f"{self.name} - {self.description}"

    @property
    def recurring_orders(self):
        return self.orders.order_by('id').exclude(recurring_period=RecurringPeriod.objects.get(name="ONE_TIME"))

    @property
    def last_recurring_order(self):
        return self.recurring_orders.last()

    @property
    def one_time_orders(self):
        return self.orders.order_by('id').filter(recurring_period=RecurringPeriod.objects.get(name="ONE_TIME"))

    @property
    def last_one_time_order(self):
        return self.one_time_orders.last()

    def create_order(self, when_to_start=None, recurring_period=None):
        billing_address = BillingAddress.get_address_for(self.owner)

        if not billing_address:
            raise ValidationError("Cannot order without a billing address")

        if not when_to_start:
            when_to_start = timezone.now()

        if not recurring_period:
            recurring_period = self.default_recurring_period


        # Create one time order if we did not create one already
        if self.one_time_price > 0 and not self.last_one_time_order:
            one_time_order = Order.objects.create(owner=self.owner,
                                                  billing_address=billing_address,
                                                  starting_date=when_to_start,
                                                  price=self.one_time_price,
                                                  recurring_period=RecurringPeriod.objects.get(name="ONE_TIME"),
                                                  description=str(self))
            self.orders.add(one_time_order)
        else:
            one_time_order = None

        if recurring_period != RecurringPeriod.objects.get(name="ONE_TIME"):
            if one_time_order:
                recurring_order = Order.objects.create(owner=self.owner,
                                                       billing_address=billing_address,
                                                       starting_date=when_to_start,
                                                       price=self.recurring_price,
                                                       recurring_period=recurring_period,
                                                       depends_on=one_time_order,
                                                       description=str(self))
            else:
                recurring_order = Order.objects.create(owner=self.owner,
                                                       billing_address=billing_address,
                                                       starting_date=when_to_start,
                                                       price=self.recurring_price,
                                                       recurring_period=recurring_period,
                                                       description=str(self))
            self.orders.add(recurring_order)


    # FIXME: this could/should be part of Order (?)
    def create_or_update_recurring_order(self, when_to_start=None, recurring_period=None):
        if not self.recurring_price:
            return

        if not recurring_period:
            recurring_period = self.default_recurring_period

        if not when_to_start:
            when_to_start = timezone.now()

        if self.last_recurring_order:
            if self.recurring_price < self.last_recurring_order.price:

                if when_to_start < self.last_recurring_order.next_cancel_or_downgrade_date:
                    when_to_start = start_after(self.last_recurring_order.next_cancel_or_downgrade_date)

            when_to_end = end_before(when_to_start)

            new_order = Order.objects.create(owner=self.owner,
                                             billing_address=self.last_recurring_order.billing_address,
                                             starting_date=when_to_start,
                                             price=self.recurring_price,
                                             recurring_period=recurring_period,
                                             description=str(self),
                                             replaces=self.last_recurring_order)

            self.last_recurring_order.replace_with(new_order)
            self.orders.add(new_order)
        else:
            self.create_order(when_to_start, recurring_period)

    @property
    def is_recurring(self):
        return self.recurring_price > 0

    @property
    def billing_address(self):
        return self.order.billing_address

    def discounted_price_by_period(self, requested_period):
        """
        Each product has a standard recurring period for which
        we define a pricing. I.e. VPN is usually year, VM is usually monthly.

        The user can opt-in to use a different period, which influences the price:
        The longer a user commits, the higher the discount.

        Products can also be limited in the available periods. For instance
        a VPN only makes sense to be bought for at least one day.

        Rules are as follows:

        given a standard recurring period of ..., changing to ... modifies price ...


        # One month for free if buying / year, compared to a month: about 8.33% discount
        per_year -> per_month -> /11
        per_month -> per_year -> *11

        # Month has 30.42 days on average. About 7.9% discount to go monthly
        per_month -> per_day    -> /28
        per_day   -> per_month  -> *28

        # Day has 24h, give one for free
        per_day   -> per_hour   -> /23
        per_hour   -> per_day   -> /23


        Examples

        VPN @ 120CHF/y becomes
        - 10.91 CHF/month (130.91 CHF/year)
        - 0.39 CHF/day (142.21 CHF/year)

        VM @ 15 CHF/month becomes
        - 165 CHF/month (13.75 CHF/month)
        - 0.54 CHF/day (16.30 CHF/month)

        """

        # FIXME: This logic needs to be phased out / replaced by product specific (?)
        # proportions. Maybe using the RecurringPeriod table to link the possible discounts/add ups

        if self.default_recurring_period == RecurringPeriod.PER_365D:
            if requested_period == RecurringPeriod.PER_365D:
                return self.recurring_price
            if requested_period == RecurringPeriod.PER_30D:
                return self.recurring_price/11.
            if requested_period == RecurringPeriod.PER_DAY:
                return self.recurring_price/11./28.

        elif self.default_recurring_period == RecurringPeriod.PER_30D:
            if requested_period == RecurringPeriod.PER_365D:
                return self.recurring_price*11
            if requested_period == RecurringPeriod.PER_30D:
                return self.recurring_price
            if requested_period == RecurringPeriod.PER_DAY:
                return self.recurring_price/28.

        elif self.default_recurring_period == RecurringPeriod.PER_DAY:
            if requested_period == RecurringPeriod.PER_365D:
                return self.recurring_price*11*28
            if requested_period == RecurringPeriod.PER_30D:
                return self.recurring_price*28
            if requested_period == RecurringPeriod.PER_DAY:
                return self.recurring_price
        else:
            # FIXME: use the right type of exception here!
            raise Exception("Did not implement the discounter for this case")


    def save(self, *args, **kwargs):
        # try:
        #     ba = BillingAddress.get_address_for(self.owner)
        # except BillingAddress.DoesNotExist:
        #     raise ValidationError("User does not have a billing address")

        # if not ba.active:
        #     raise ValidationError("User does not have an active billing address")


        # Verify the required JSON fields

        super().save(*args, **kwargs)



###
# Orders.

class Order(models.Model):
    """
    Order are assumed IMMUTABLE and used as SOURCE OF TRUST for generating
    bills. Do **NOT** mutate then!

    An one time order is "closed" (does not need to be billed anymore)
    if it has one bill record. Having more than one is a programming
    error.

    A recurring order is closed if it has been replaced
    (replaces__isnull=False) AND the ending_date is set AND it was
    billed the last time it needed to be billed (how to check the last
    item?)

    BOTH are closed, if they are ended/closed AND have been fully
    charged.

    Fully charged == fully billed: sum_of_order_usage == sum_of_bill_records

    """

    owner = models.ForeignKey(get_user_model(),
                              on_delete=models.CASCADE,
                              editable=True)

    billing_address = models.ForeignKey(BillingAddress,
                                        on_delete=models.CASCADE)

    description = models.TextField()

    product = models.ForeignKey(Product, blank=False, null=False, on_delete=models.CASCADE)
    config = models.JSONField()

    creation_date = models.DateTimeField(auto_now_add=True)
    starting_date = models.DateTimeField(default=timezone.now)
    ending_date = models.DateTimeField(blank=True, null=True)

    recurring_period = models.ForeignKey(RecurringPeriod,
                                         on_delete=models.CASCADE,
                                         editable=True)

    one_time_price = models.DecimalField(default=0.0,
                                max_digits=AMOUNT_MAX_DIGITS,
                                decimal_places=AMOUNT_DECIMALS,
                                validators=[MinValueValidator(0)])

    recurring_price = models.DecimalField(default=0.0,
                                max_digits=AMOUNT_MAX_DIGITS,
                                decimal_places=AMOUNT_DECIMALS,
                                validators=[MinValueValidator(0)])

    currency = models.CharField(max_length=32, choices=Currency.choices, default=Currency.CHF)

    replaces = models.ForeignKey('self',
                                 related_name='replaced_by',
                                 on_delete=models.CASCADE,
                                 blank=True,
                                 null=True)

    depends_on = models.ForeignKey('self',
                                   related_name='parent_of',
                                   on_delete=models.CASCADE,
                                   blank=True,
                                   null=True)

    should_be_billed = models.BooleanField(default=True)

    @property
    def earliest_ending_date(self):
        """
        Recurring orders cannot end before finishing at least one recurring period.

        One time orders have a recurring period of 0, so this work universally
        """

        return self.starting_date + datetime.timedelta(seconds=self.recurring_period.duration_seconds)


    def next_cancel_or_downgrade_date(self, until_when=None):
        """
        Return the next proper ending date after n times the
        recurring_period, where n is an integer that applies for downgrading
        or cancelling.
        """

        if not until_when:
            until_when = timezone.now()

        if until_when < self.starting_date:
            raise ValidationError("Cannot end before start of start of order")

        if self.recurring_period.duration_seconds > 0:
            delta = until_when - self.starting_date

            num_times = ceil(delta.total_seconds() / self.recurring_period.duration_seconds)

            next_date = self.starting_date + datetime.timedelta(seconds=num_times * self.recurring_period.duration_seconds)
        else:
            next_date = self.starting_date

        return next_date

    def get_ending_date_for_bill(self, bill):
        """
        Determine the ending date given a specific bill
        """

        # If the order is quit, charge the final amount / finish (????)
        # Probably not a good idea -- FIXME :continue until usual
        if self.ending_date:
            this_ending_date = self.ending_date
        else:
            if self.next_cancel_or_downgrade_date(bill.ending_date) > bill.ending_date:
                this_ending_date = self.next_cancel_or_downgrade_date(bill.ending_date)
            else:
                this_ending_date = bill.ending_date

        return this_ending_date


    @property
    def count_billed(self):
        """
        How many times this order was billed so far.
        This logic is mainly thought to be for recurring bills, but also works for one time bills
        """

        return sum([ br.quantity for br in self.bill_records.all() ])

    def count_used(self, when=None):
        """
        How many times this order was billed so far.
        This logic is mainly thought to be for recurring bills, but also works for one time bills
        """

        if self.is_one_time:
            return 1

        if not when:
            when = timezone.now()

        # Cannot be used after it ended
        if self.ending_date and when > self.ending_date:
            when = self.ending_date

        return (when - self.starting_date) / self.default_recurring_period

    @property
    def all_usage_billed(self, when=None):
        """
        Returns true if this order does not need any further billing
        ever. In other words: is this order "closed"?
        """

        if self.count_billed == self.count_used(when):
            return True
        else:
            return False

    @property
    def is_closed(self):
        if self.all_usage_billed and self.ending_date:
            return True
        else:
            return False

    @property
    def is_recurring(self):
        return not self.recurring_period == RecurringPeriod.objects.get(name="ONE_TIME")

    @property
    def is_one_time(self):
        return not self.is_recurring

    def replace_with(self, new_order):
        new_order.replaces = self
        self.ending_date = end_before(new_order.starting_date)
        self.save()

    def update_order(self, config, starting_date=None):
        """
        Updating an order means creating a new order and reference the previous order
        """

        if not starting_date:
            starting_date = timezone.now()

        new_order = self.__class__(owner=self.owner,
                                   billing_address=self.billing_address,
                                   description=self.description,
                                   product=self.product,
                                   config=config,
                                   starting_date=starting_date,
                                   currency=self.currency
                                   )

        (new_order.one_time_price, new_order.recurring_price, new_order.config) = new_order.calculate_prices_and_config()



        new_order.replaces = self
        new_order.save()

        self.ending_date = end_before(new_order.starting_date)
        self.save()

        return new_order


    def create_bill_record(self, bill):
        br = None

        # Note: check for != 0 not > 0, as we allow discounts to be expressed with < 0
        if self.one_time_price != 0 and self.billrecord_set.count() == 0:
            br = BillRecord.objects.create(bill=bill,
                                           order=self,
                                           starting_date=self.starting_date,
                                           ending_date=self.starting_date,
                                           is_recurring_record=False)

        if self.recurring_price != 0:
            br = BillRecord.objects.filter(bill=bill, order=self, is_recurring_record=True).first()

            if br:
                self.update_bill_record_for_recurring_order(br, bill)
            else:
                br = self.create_new_bill_record_for_recurring_order(bill)

        return br

    def update_bill_record_for_recurring_order(self,
                                         bill_record,
                                         bill):
        """
        Possibly update a bill record according to the information in the bill
        """

        # If the order has an ending date set, we might need to adjust the bill_record
        if self.ending_date:
            if bill_record_for_this_bill.ending_date != self.ending_date:
                bill_record_for_this_bill.ending_date = self.ending_date

            else:
                # recurring, not terminated, should go until at least end of bill
                if bill_record_for_this_bill.ending_date < bill.ending_date:
                    bill_record_for_this_bill.ending_date = bill.ending_date

                bill_record_for_this_bill.save()

    def create_new_bill_record_for_recurring_order(self, bill):
        """
        Create a new bill record
        """

        last_bill_record = BillRecord.objects.filter(order=self, is_recurring_record=True).order_by('id').last()

        starting_date=self.starting_date

        if last_bill_record:
            # We already charged beyond the end of this bill's period
            if last_bill_record.ending_date >= bill.ending_date:
                return

            # This order is terminated or replaced
            if self.ending_date:
                # And the last bill record already covered us -> nothing to be done anymore
                if last_bill_record.ending_date == self.ending_date:
                    return

            starting_date = start_after(last_bill_record.ending_date)

        ending_date = self.get_ending_date_for_bill(bill)

        return BillRecord.objects.create(bill=bill,
                                         order=self,
                                         starting_date=starting_date,
                                         ending_date=ending_date,
                                         is_recurring_record=True)

    def calculate_prices_and_config(self):
        one_time_price = 0
        recurring_price = 0

        if self.config:
            config = self.config

            if 'features' not in self.config:
                self.config['features'] = {}

        else:
            config = {
                'features': {}
            }

        # FIXME: adjust prices to the selected recurring_period to the

        if 'features' in self.product.config:
            for feature in self.product.config['features']:

                # Set min to 0 if not specified
                min_val = self.product.config['features'][feature].get('min', 0)

                # We might not even have 'features' cannot use .get() on it
                try:
                    value = self.config['features'][feature]
                except (KeyError, TypeError):
                    value = self.product.config['features'][feature]['min']

                # Set max to current value if not specified
                max_val = self.product.config['features'][feature].get('max', value)


                if value < min_val or value > max_val:
                    raise ValidationError(f"Feature '{feature}' must be at least {min_val} and at maximum {max_val}. Value is: {value}")

                one_time_price += self.product.config['features'][feature]['one_time_price_per_unit'] *  value
                recurring_price += self.product.config['features'][feature]['recurring_price_per_unit'] *  value
                config['features'][feature] = value

        return (one_time_price, recurring_price, config)

    def check_parameters(self):
        if 'parameters' in self.product.config:
            for parameter in self.product.config['parameters']:
                if not parameter in self.config['parameters']:
                    raise ValidationError(f"Required parameter '{parameter}' is missing.")


    def save(self, *args, **kwargs):
        # Calculate the price of the order when we create it
        # IMMUTABLE fields -- need to create new order to modify them
        # However this is not enforced here...
        if self._state.adding:
            (self.one_time_price, self.recurring_price, self.config) = self.calculate_prices_and_config()

            if self.recurring_period_id is None:
                self.recurring_period = self.product.default_recurring_period

            try:
                prod_period = self.product.recurring_periods.get(producttorecurringperiod__recurring_period=self.recurring_period)
            except ObjectDoesNotExist:
                raise ValidationError(f"Recurring Period {self.recurring_period} not allowed for product {self.product}")

        self.check_parameters()

        if self.ending_date and self.ending_date < self.starting_date:
            raise ValidationError("End date cannot be before starting date")


        super().save(*args, **kwargs)


    def __str__(self):
        try:
            conf = " ".join([ f"{key}:{val}" for key,val in self.config['features'].items() if val != 0 ])
        except KeyError:
            conf = ""

        return f"Order {self.id}: {self.description} {conf}"

class Bill(models.Model):
    """
    A bill is a representation of usage at a specific time
    """
    owner = models.ForeignKey(get_user_model(),
            on_delete=models.CASCADE)

    creation_date = models.DateTimeField(auto_now_add=True)
    starting_date = models.DateTimeField(default=start_of_this_month)
    ending_date = models.DateTimeField()
    due_date = models.DateField(default=default_payment_delay)


    billing_address = models.ForeignKey(BillingAddress,
                                        on_delete=models.CASCADE,
                                        editable=True,
                                        null=False)

    # FIXME: editable=True -> is in the admin, but also editable in DRF
    # Maybe filter fields in the serializer?

    is_final = models.BooleanField(default=False)

    class Meta:
        constraints = [
            models.UniqueConstraint(fields=['owner',
                                            'starting_date',
                                            'ending_date' ],
                                    name='one_bill_per_month_per_user')
        ]

    def close(self):
        """
        Close/finish a bill
        """

        self.is_final = True
        self.save()

    @property
    def sum(self):
        bill_records = BillRecord.objects.filter(bill=self)
        return sum([ br.sum for br in bill_records ])

    @property
    def vat_rate(self):
        """
        Handling VAT is a tricky business - thus we only implement the cases
        that we clearly now and leave it open to fellow developers to implement
        correct handling for other cases.

        Case CH:

        - If the customer is in .ch -> apply standard rate
        - If the customer is in EU AND private -> apply country specific rate
        - If the customer is in EU AND business -> do not apply VAT
        - If the customer is outside EU and outside CH -> do not apply VAT
        """

        provider = UncloudProvider.objects.get()

        # Assume always VAT inside the country
        if provider.country == self.billing_address.country:
            vat_rate = VATRate.objects.get(country=provider.country,
                                       when=self.ending_date)
        elif self.billing_address.country in EU:
            # FIXME: need to check for validated vat number
            if self.billing_address.vat_number:
                return 0
            else:
                return VATRate.objects.get(country=self.biling_address.country,
                                           when=self.ending_date)
        else: # non-EU, non-national
            return 0


    @classmethod
    def create_bills_for_all_users(cls):
        """
        Create next bill for each user
        """

        for owner in get_user_model().objects.all():
            cls.create_next_bills_for_user(owner)

    @classmethod
    def create_next_bills_for_user(cls, owner, ending_date=None):
        """
        Create one bill per billing address, as the VAT rates might be different
        for each address
        """

        bills = []

        for billing_address in BillingAddress.objects.filter(owner=owner):
            bills.append(cls.create_next_bill_for_user_address(billing_address, ending_date))

        return bills

    @classmethod
    def create_next_bill_for_user_address(cls, billing_address, ending_date=None):
        """
        Create the next bill for a specific billing address of a user
        """

        owner = billing_address.owner

        all_orders = Order.objects.filter(owner=owner,
                                          billing_address=billing_address).order_by('id')

        bill = cls.get_or_create_bill(billing_address, ending_date=ending_date)

        for order in all_orders:
            order.create_bill_record(bill)

        return bill


    @classmethod
    def get_or_create_bill(cls, billing_address, ending_date=None):
        """
        Get / reuse last bill if it is not yet closed

        Create bill, if there is no bill or if bill is closed.
        """

        last_bill = cls.objects.filter(billing_address=billing_address).order_by('id').last()

        all_orders = Order.objects.filter(billing_address=billing_address).order_by('id')
        first_order = all_orders.first()

        bill = None

        # Get date & bill from previous bill, if it exists
        if last_bill:
            if not last_bill.is_final:
                bill = last_bill
                starting_date = last_bill.starting_date
                ending_date = bill.ending_date
            else:
                starting_date = last_bill.ending_date + datetime.timedelta(seconds=1)
        else:
            # Might be an idea to make this the start of the month, too
            if first_order:
                starting_date = first_order.starting_date
            else:
                starting_date = timezone.now()

        if not ending_date:
            ending_date = end_of_month(starting_date)

        if not bill:
            bill = cls.objects.create(
                owner=billing_address.owner,
                starting_date=starting_date,
                ending_date=ending_date,
                billing_address=billing_address)


        return bill

    def __str__(self):
        return f"Bill {self.owner}-{self.id}"


class BillRecord(models.Model):
    """
    Entry of a bill, dynamically generated from an order.
    """

    bill = models.ForeignKey(Bill, on_delete=models.CASCADE)
    order = models.ForeignKey(Order, on_delete=models.CASCADE)

    creation_date = models.DateTimeField(auto_now_add=True)
    starting_date = models.DateTimeField()
    ending_date = models.DateTimeField()

    is_recurring_record = models.BooleanField(blank=False, null=False)

    @property
    def quantity(self):
        """ Determine the quantity by the duration"""
        if not self.is_recurring_record:
            return 1

        record_delta = self.ending_date - self.starting_date

        return record_delta.total_seconds()/self.order.recurring_period.duration_seconds

    @property
    def sum(self):
        if self.is_recurring_record:
            return self.order.recurring_price * Decimal(self.quantity)
        else:
            return self.order.one_time_price

    @property
    def price(self):
        if self.is_recurring_record:
            return self.order.recurring_price
        else:
            return self.order.one_time_price

    def __str__(self):
        if self.is_recurring_record:
            bill_line =  f"{self.starting_date} - {self.ending_date}: {self.quantity} x {self.order}"
        else:
            bill_line =  f"{self.starting_date}: {self.order}"

        return bill_line

    def save(self, *args, **kwargs):
        if self.ending_date < self.starting_date:
            raise ValidationError("End date cannot be before starting date")

        super().save(*args, **kwargs)


class ProductToRecurringPeriod(models.Model):
    """
    Intermediate manytomany mapping class that allows storing the default recurring period
    for a product
    """

    recurring_period = models.ForeignKey(RecurringPeriod, on_delete=models.CASCADE)
    product = models.ForeignKey(Product, on_delete=models.CASCADE)

    is_default = models.BooleanField(default=False)

    class Meta:
        constraints = [
            models.UniqueConstraint(fields=['product'],
                                    condition=Q(is_default=True),
                                    name='one_default_recurring_period_per_product'),
            models.UniqueConstraint(fields=['product', 'recurring_period'],
                                    name='recurring_period_once_per_product')
        ]

    def __str__(self):
        return f"{self.product} - {self.recurring_period} (default: {self.is_default})"