import logging
import datetime
import json

from math import ceil
from calendar import monthrange
from decimal import Decimal

from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.validators import MinValueValidator
from django.db import models
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
# Verify whether or not to use them here
from django.core.exceptions import ObjectDoesNotExist, ValidationError

import uncloud_pay
from uncloud import AMOUNT_DECIMALS, AMOUNT_MAX_DIGITS
from uncloud.models import UncloudAddress, UncloudProvider
from uncloud.selectors import filter_for_when
from .services import *

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

EU_COUNTRIES = ['at', 'be', 'bg', 'ch', 'cy', 'cz', 'hr', 'dk',
                'ee', 'fi', 'fr', 'mc', 'de', 'gr', 'hu', 'ie', 'it',
                'lv', 'lu', 'mt', 'nl', 'po', 'pt', 'ro','sk', 'si', 'es',
                'se', 'gb']


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

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')


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


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

    card_name = models.CharField(null=False, max_length=128, default="")
    card_id = models.CharField(null=False, max_length=32)
    last4 = models.CharField(null=False, max_length=4)
    brand = models.CharField(null=False, max_length=64)
    expiry_date = models.DateField(null=False)
    active = models.BooleanField(default=False)

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


    def __str__(self):
        return f"{self.card_name}: {self.brand} {self.last4} ({self.expiry_date})"

    def delete(self, **kwargs):
        uncloud_pay.stripe.delete_card(self.card_id)
        super().delete(**kwargs)


    def activate(self):
        StripeCreditCard.objects.filter(owner=self.owner, active=True).update(active=False)
        self.active = True
        self.save()

class Payment(models.Model):
    owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
    type = models.CharField(max_length=256,
                              choices = (
                                  ('withdraw', 'Withdraw Money'),
                                  ('deposit', 'Deposit Money')
                              ), null=False, blank=False, default="deposit")

    notes = models.TextField(default="", null=True, blank=True)

    amount = models.DecimalField(
            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'),
                              ), null=True, blank=True,)

    timestamp = models.DateTimeField(default=timezone.now)

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

    external_reference = models.CharField(max_length=256, default="", null=True, blank=True)

    def __str__(self):
        return f"{self.amount}{self.currency} from {self.owner} via {self.source} on {self.timestamp}"

    @classmethod
    def deposit(cls, owner, amount, source, currency='CHF', notes=''):
        if source == 'stripe':
            try:
                payment_intent = uncloud_pay.stripe.charge_customer(owner, amount, currency)
                if not payment_intent.status or payment_intent.status != 'succeeded':
                    raise Exception("The payment has been failed, please try to activate another card")
                return cls.objects.create(owner=owner, type="deposit", amount=amount, external_reference=payment_intent["id"],
                                    currency=currency, source=source, notes=notes)
            except Exception as e:
                raise e

    @classmethod
    def withdraw(cls, owner, amount, currency='CHF', notes=''):
        return cls.objects.create(owner=owner, type="withdraw", amount=amount,
                                    currency=currency, notes=notes)



# 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, related_name='billing_addresses')
    vat_number = models.CharField(max_length=100, default="", blank=True)
    vat_number_verified = models.BooleanField(default=False)
    vat_number_validated_on = models.DateTimeField(blank=True, null=True)
    stripe_tax_id = models.CharField(max_length=100, default="", blank=True)
    active = models.BooleanField(default=False)

    class Meta:
        constraints = [
            models.UniqueConstraint(fields=['owner'],
                                    condition=models.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",
                                                 full_name="Uncloud Admin",
                                                 street="Uncloudstreet. 42",
                                                 city="Luchsingen",
                                                 postal_code="8775",
                                                 country="CH",
                                                 active=True)

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

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

###
# 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_vat_rate_for_country(country, when=None):
        """
        Returns the VAT rate for business to customer.

        B2B is always 0% with the exception of trading within the own country
        """
        vatrate = filter_for_when(VATRate.objects.filter(territory_codes=country), when).first()

        # By default we charge VAT. This affects:
        # - Same country sales (VAT applied)
        # - B2C to EU  (VAT applied)
        rate = vatrate.rate if vatrate else 0
        if not country.lower().strip() in EU_COUNTRIES:
            rate = 0

        return rate

    @staticmethod
    def get_vat_rate(billing_address, when=None):
        rate = VATRate.get_vat_rate_for_country(billing_address.country, when)

        # Exception: if...
        # - the billing_address is in EU,
        # - the vat_number has been set
        # - the vat_number has been verified
        # Then we do not charge VAT

        if rate != 0 and billing_address.vat_number and billing_address.vat_number_verified:
            rate = 0
        return rate


    def __str__(self):
        return f"{self.territory_codes}: {self.starting_date} - {self.ending_date or ''}: {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
                                                          },
                                                         'ram_gb':
                                                         { 'min': 1,
                                                           'max': 256
                                                          },
                                                         'ssd_gb':
                                                         { 'min': 10
                                                          },
                                                         'hdd_gb':
                                                         { 'min': 0,
                                                          },
                                                         'additional_ipv4_address':
                                                         { 'min': 0,
                                                          },
                                                     }
                                                 }
                                                 )

        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,
                                                          },
                                                         'cores':
                                                         { 'min': 1,
                                                           'max': 48,
                                                          },
                                                         'ram_gb':
                                                         { 'min': 1,
                                                           'max': 256,
                                                          },
                                                         'ssd_gb':
                                                         { 'min': 10
                                                          },
                                                         'hdd_gb':
                                                         { 'min': 0
                                                          },
                                                         'additional_ipv4_address':
                                                         { 'min': 0,},
                                                     }
                                                 }
                                                 )

        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}"

    @property
    def recurring_orders(self):
        return self.orders.order_by('id').exclude(recurring_price=0)

    @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_price=0)

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

    # 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)


###
# Pricing
######
import logging

from django.db import models

logger = logging.getLogger(__name__)

class PricingPlan(models.Model):
    name = models.CharField(max_length=255, unique=True)
    vat_inclusive = models.BooleanField(default=True)
    vat_percentage = models.DecimalField(
        max_digits=7, decimal_places=5, blank=True, default=0
    )
    set_up_fees = models.DecimalField(
        max_digits=7, decimal_places=2, default=0
    )
    monthly_maintenance_fees = models.DecimalField(
        max_digits=7, decimal_places=2, default=0
    )
    cores_unit_price = models.DecimalField(
        max_digits=7, decimal_places=2, default=0
    )
    ram_unit_price = models.DecimalField(
        max_digits=7, decimal_places=2, default=0
    )
    storage_hd_unit_price = models.DecimalField(
        max_digits=7, decimal_places=2, default=0
    )
    storage_ssd_unit_price = models.DecimalField(
        max_digits=7, decimal_places=2, default=0
    )
    discount_name = models.CharField(max_length=255, null=True, blank=True)
    discount_amount = models.DecimalField(
        max_digits=6, decimal_places=2, default=0
    )
    stripe_coupon_id = models.CharField(max_length=255, null=True, blank=True)

    def __str__(self):
        display_str = self.name + ' => ' + ' - '.join([
            '{} Setup'.format(self.set_up_fees.normalize()),
            '{}/Core'.format(self.cores_unit_price.normalize()),
            '{}/GB RAM'.format(self.ram_unit_price.normalize()),
            '{}/GB SSD'.format(self.storage_ssd_unit_price.normalize()),
            '{}/GB HD'.format(self.storage_hd_unit_price.normalize()),
            '{}% VAT'.format(self.vat_percentage.normalize())
            if not self.vat_inclusive else 'VAT-Incl',
        ])
        if self.discount_amount:
            display_str = ' - '.join([
                display_str,
                '{} {}'.format(
                    self.discount_amount,
                    self.discount_name if self.discount_name else 'Discount'
                )
            ])
        return display_str

    @classmethod
    def get_by_name(cls, name):
        try:
            pricing = PricingPlan.objects.get(name=name)
        except Exception as e:
            logger.error(
                "Error getting VMPricing with name {name}. "
                "Details: {details}. Attempting to return default"
                "pricing.".format(name=name, details=str(e))
            )
            pricing = PricingPlan.get_default_pricing()
        return pricing

    @classmethod
    def get_default_pricing(cls):
        """ Returns the default pricing or None """
        try:
            default_pricing = PricingPlan.objects.get(name='default')
        except Exception as e:
            logger.error(str(e))
            default_pricing = None
        return default_pricing

###
# 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)

    # Let's forget about this one
    customer = models.ForeignKey(StripeCustomer, on_delete=models.CASCADE, null=True)

    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)
    pricing_plan = models.ForeignKey(PricingPlan, blank=False, null=True, on_delete=models.CASCADE)

    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 cancel(self):
        self.ending_date = timezone.now()
        self.should_be_billed = False
        self.save()

    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_recurring(self):
        return self.recurring_price > 0

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

    @property
    def description(self):
        desc = self.product.description + "( "
        config = json.loads(self.config)
        if config and config["cores"]:
            desc = f"{desc} {config['cores']} Cores,"
        if config and config["memory"]:
            desc = f"{desc} {config['memory']} RAM,"
        if config and config["storage"]:
            desc = f"{desc} {config['storage']} GB SSD,"
        desc += " )"
        return desc

    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,
                                   product=self.product,
                                   config=config,
                                   pricing_plan=self.pricing_plan,
                                   starting_date=starting_date,
                                   currency=self.currency
                                   )

        new_order.recurring_price = new_order.calculate_recurring_price()
        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
        if self.recurring_price != 0:
            records = BillRecord.objects.filter(order=self).all()
            if not records:
                if self.one_time_price:
                    br = BillRecord.objects.create(bill=bill,
                                            order=self,
                                            starting_date=self.starting_date,
                                            ending_date=bill.ending_date,
                                            is_recurring_record=False)
                else:
                    br = self.create_new_bill_record_for_recurring_order(bill)
            else:
                opened_recurring_record = BillRecord.objects.filter(bill=bill, order=self, is_recurring_record=True).first()
                if opened_recurring_record:
                    br = opened_recurring_record
                    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.ending_date != self.ending_date:
                bill_record.ending_date = self.ending_date

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

                bill_record.save()

    def create_new_bill_record_for_recurring_order(self, bill):
        """
        Create a new bill record
        """
        last_bill_record = BillRecord.objects.filter(order=self).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_recurring_price(self):
        try:
            config = json.loads(self.config)
            recurring_price = self.pricing_plan.monthly_maintenance_fees
            if 'cores' in config:
                recurring_price += self.pricing_plan.cores_unit_price * int(config['cores'])
            if 'memory' in config:
                recurring_price += self.pricing_plan.ram_unit_price * int(config['memory'])
            if 'storage' in config:
                #TODO Fix the ssd static value
                recurring_price += (10 * self.pricing_plan.storage_ssd_unit_price) + (self.pricing_plan.storage_hd_unit_price * int(config['storage']))

            vat_rate = VATRate.get_vat_rate(self.billing_address)
            vat_validation_status = "verified" if self.billing_address.vat_number_validated_on and self.billing_address.vat_number_verified else False
            subtotal, subtotal_after_discount, price_after_discount_with_vat, vat, vat_percent, vat_amount, discount = uncloud_pay.utils.apply_vat_discount(
                recurring_price, self.pricing_plan,
                vat_rate=vat_rate * 100, vat_validation_status = vat_validation_status
            )
            return price_after_discount_with_vat
        except Exception as e:
            logger.error("An error occurred while parsing the config obj", e)
            return 0

    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.recurring_price = self.calculate_recurring_price()

            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):
        return f"Order {self.id}: {self.description}"

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)

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

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

    status = models.CharField(max_length=32, choices= (
                                  ('new', 'New'),
                                  ('cancelled', 'Cancelled'),
                                  ('paid', 'Paid')
                              ), null=False, blank=False, default="new")

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

    def close(self, status):
        """
        Close/finish a bill
        """
        self.status = status
        if not self.ending_date:
            self.ending_date = timezone.now()
        self.save()

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

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

    @property
    def vat_amount(self):
        return round(self.vat_rate * self.subtotal, 2)

    @property
    def vat_rate(self):
        return VATRate.get_vat_rate(self.billing_address, when=self.ending_date)


    @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):
            bill = cls.create_next_bill_for_user_address(billing_address, ending_date)
            if bill:
                bills.append(bill)

        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(Q(owner__id=owner.id), Q(should_be_billed=True),
                                          Q(billing_address__id=billing_address.id)
                                         ).order_by('id')

        if len(all_orders) > 0:
            bill = cls.get_or_create_bill(billing_address, ending_date=ending_date)
            for order in all_orders:
                order.create_bill_record(bill)
            return bill
        else:
            # This Customer Hasn't any active orders
            return False

    @classmethod
    def create_next_bill_for_order(cls, order, ending_date=None):
        """
        Create the next bill for a specific order of a user
        """
        bill = cls.get_or_create_bill(order.billing_address, ending_date=ending_date)
        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 last_bill.status == 'new':
                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"{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, related_name='bill_records')
    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.date() - self.starting_date.date()
        if self.order.recurring_period and self.order.recurring_period.duration_seconds > 0:
            return int(record_delta.total_seconds() / self.order.recurring_period.duration_seconds)
        else:
            return 1

    @property
    def sum(self):
        if self.is_recurring_record:
            return round(float(self.order.recurring_price) * self.quantity, 2)
        else:
            return self.order.one_time_price

    @property
    def description(self):
        if self.order:
            return self.order.description
        return ''

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

    @property
    def subtotal(self):
        billing_address_ins = self.order.billing_address
        vat_rate = VATRate.get_vat_rate(billing_address_ins)
        vat_validation_status = "verified" if billing_address_ins.vat_number_validated_on and billing_address_ins.vat_number_verified else False
        config = json.loads(self.order.config)
        pricing = uncloud_pay.utils.get_order_total_with_vat(
            config["cores"], config["memory"], config["storage"], self.order.pricing_plan.name,
            vat_rate=vat_rate * 100, vat_validation_status = vat_validation_status
        )
        return pricing['subtotal_after_discount']

    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=models.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})"


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

    starting_date = models.DateField(blank=True, null=True)
    ending_date = models.DateField(blank=True, null=True)


    @classmethod
    def user_has_membership(user, when):
        """
        Return true if user has membership at a point of time,
        return false if that is not the case
        """

        pass

        # cls.objects.filter(owner=user,
        #                    starting_date)
