import datetime
from itertools import chain

from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.db import models, transaction
import markdown


class Document(models.Model):
    """ A document is a scenario or a record from facts, on 1 month.
    """
    TYPE_FACT = 'fact'
    TYPE_PLAN = 'plan'

    name = models.CharField(max_length=130)
    comment = models.TextField(blank=True, help_text="Texte brut ou markdown")
    comment_html = models.TextField(blank=True, editable=False)
    date = models.DateField(default=datetime.datetime.now)
    type = models.CharField(max_length=10, choices=(
        (TYPE_FACT, 'relevé'),
        (TYPE_PLAN, 'scénario/estimation'),
    ))

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        self.comment_html = markdown.markdown(self.comment)
        super().save(*args, **kwargs)

    def get_absolute_url(self):
        return reverse('detail-document', kwargs={'pk': self.pk})

    def copy(self):
        """ Deep copy and saving of a document

        All related resources are copied.
        """
        with transaction.atomic():
            new_doc = Document.objects.get(pk=self.pk)
            new_doc.pk = None
            new_doc.name = 'COPIE DE {}'.format(new_doc.name)
            new_doc.save()

            new_services, new_costs, new_goods = {}, {}, {}

            to_copy = (
                (self.service_set, new_services),
                (self.good_set, new_goods),
                (self.cost_set, new_costs),
            )

            for qs, index in to_copy:
                for item in qs.all():
                    old_pk = item.pk
                    item.pk = None
                    item.document = new_doc
                    item.save()
                    index[old_pk] = item

            to_reproduce = (
                (ServiceUse, new_services),
                (GoodUse, new_goods),
                (CostUse, new_costs),
            )

            for Klass, index in to_reproduce:
                src_qs = Klass.objects.filter(service__document=self)
                for use in src_qs.all():
                    use.pk = None
                    use.service = new_services[use.service.pk]
                    use.resource = index[use.resource.pk]
                    use.save()

            return new_doc


class AbstractItem(models.Model):
    name = models.CharField(max_length=130)
    description = models.TextField(blank=True)
    description_html = models.TextField(blank=True)
    document = models.ForeignKey(Document)

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        self.description_html = markdown.markdown(self.description)
        super().save(*args, **kwargs)

    class Meta:
        abstract = True


class AbstractResource(AbstractItem):
    UNIT_AMP = 'a'
    UNIT_MBPS = 'mbps'
    UNIT_U = 'u'
    UNIT_IPV4 = 'ipv4'
    UNIT_ETHERNET_PORT = 'eth'
    UNIT_SERVICE = 'services'

    capacity_unit = models.CharField(
        max_length=10,
        choices=(
            (UNIT_AMP, 'A'),
            (UNIT_MBPS, 'Mbps'),
            (UNIT_U, 'U'),
            (UNIT_IPV4, 'IPv4'),
            (UNIT_ETHERNET_PORT, 'ports'),
            (UNIT_SERVICE, 'abonnement'),
        ),
        blank=True,
    )
    total_capacity = models.FloatField(default=1)

    class Meta:
        abstract = True

    def get_use_class(self):
        raise NotImplemented

    def used(self, except_by=None):
        """ Return the used fraction of an item

        :type: Service
        :param except_by: exclude this service from the math
        :rtype: float
        """
        sharing_costs = self.get_use_class().objects.filter(resource=self)
        if except_by:
            sharing_costs = sharing_costs.exclude(service=except_by)

        existing_uses_sum = sum(
            sharing_costs.values_list('share', flat=True))

        return existing_uses_sum

    def used_fraction(self, *args, **kwargs):
        return self.used(*args, **kwargs)/self.total_capacity

    def unused(self):
        return self.total_capacity-self.used()

    def __str__(self):
        if self.capacity_unit == '':
            return self.name
        else:
            return '{} {:.0f} {}'.format(
                self.name, self.total_capacity,
                self.get_capacity_unit_display())


class Cost(AbstractResource):
    """ A monthtly cost we have to pay
    """
    price = models.FloatField(help_text="Coût mensuel")

    def get_use_class(self):
        return CostUse

    class Meta:
        verbose_name = 'Coût'


class Good(AbstractResource):
    """ A good, which replacement is provisioned
    """
    price = models.FloatField()
    provisioning_duration = models.DurationField(
        choices=settings.PROVISIONING_DURATIONS)

    def get_use_class(self):
        return GoodUse

    def monthly_provision(self):
        return self.price/self.provisioning_duration.days*(365.25/12)

    class Meta:
        verbose_name = 'Bien'


class AbstractUse(models.Model):
    share = models.FloatField()
    service = models.ForeignKey('Service')

    class Meta:
        abstract = True

    def clean(self):
        if hasattr(self, 'resource'):
            usage = self.resource.used(except_by=self.service) + self.share
            if usage > self.resource.total_capacity:
                raise ValidationError(
                    "Cannot use more than 100% of {})".format(self.resource))

    def real_share(self):
        """The share, + wasted space share

        Taking into account that the unused space is
        wasted and has to be divided among actual users
        """
        return (
            self.share +
            (self.share/self.resource.used())*self.resource.unused()
        )

    def unit_share(self):
        if self.service.subscriptions_count == 0:
            return 0
        else:
            return self.share/self.service.subscriptions_count

        return

    def unit_real_share(self):
        if self.service.subscriptions_count == 0:
            return 0
        else:
            return self.real_share()/self.service.subscriptions_count

    def value_share(self):
        return (
            self.resource.price
            * self.real_share()
            / self.resource.total_capacity
        )

    def unit_value_share(self):
        if self.service.subscriptions_count == 0:
            return 0
        else:
            return self.value_share()/self.service.subscriptions_count


class CostUse(AbstractUse):
    resource = models.ForeignKey(Cost)

    def cost_share(self):
        return (
            self.real_share() / self.resource.total_capacity
            * self.resource.price
        )

    def unit_cost_share(self):
        subscriptions_count = self.service.subscriptions_count
        if subscriptions_count == 0:
            return 0
        else:
            return self.cost_share()/self.service.subscriptions_count


class GoodUse(AbstractUse):
    resource = models.ForeignKey(Good)

    def monthly_provision_share(self):
        return (
            self.real_share()
            * self.resource.monthly_provision()
            / self.resource.total_capacity)

    def unit_monthly_provision_share(self):
        subscriptions_count = self.service.subscriptions_count
        monthly_share = self.monthly_provision_share()
        if subscriptions_count == 0:
            return 0
        else:
            return monthly_share/subscriptions_count


class Service(AbstractResource):
    """ A service we sell

    (considered monthly)
    """
    costs = models.ManyToManyField(
        Cost,
        through=CostUse,
        related_name='using_services')
    goods = models.ManyToManyField(
        Good,
        through=GoodUse,
        related_name='using_services')

    subscriptions_count = models.PositiveIntegerField(default=0)
    reusable = models.BooleanField(
        default=False,
        help_text="Peut-être utilisé par d'autres services")

    @property
    def price(self):
        return self.get_prices()['total_recurring_price']

    def save(self, *args, **kwargs):
        if self.reusable:
            self.capacity_unit = self.UNIT_SERVICE
            self.total_capacity = self.subscriptions_count
        return super().save(*args, **kwargs)

    def get_absolute_url(self):
        return reverse('detail-service', kwargs={'pk': self.pk})

    def get_use_class(self):
        return ServiceUse

    def get_prices(self):
        costs_uses = CostUse.objects.filter(service=self)
        goods_uses = GoodUse.objects.filter(service=self)
        services_uses = ServiceUse.objects.filter(service=self)

        total_recurring_price = sum(chain(
            (i.monthly_provision_share() for i in goods_uses),
            (i.cost_share() for i in costs_uses),
            (i.cost_share() for i in services_uses)
        ))

        total_goods_value_share = sum(
            (i.value_share() for i in chain(goods_uses, services_uses)))

        if self.subscriptions_count == 0:
            unit_recurring_price = 0
            unit_goods_value_share = 0
        else:
            unit_recurring_price = total_recurring_price/self.subscriptions_count
            unit_goods_value_share = \
                total_goods_value_share/self.subscriptions_count
        return {
            'total_recurring_price': total_recurring_price,
            'unit_recurring_price': unit_recurring_price,
            'total_goods_value_share': total_goods_value_share,
            'unit_goods_value_share': unit_goods_value_share,
        }


def validate_reusable_service(v):
    if not Service.objects.get(pk=v).reusable:
        raise ValidationError('{} is not a reusable service'.format(v))


class ServiceUse(AbstractUse):
    resource = models.ForeignKey(
        Service, related_name='dependent_services',
        limit_choices_to={'reusable': True},
        validators=[validate_reusable_service])

    def cost_share(self):
        return (
            self.share / self.resource.total_capacity
            * self.resource.price
        )

    def unit_cost_share(self):
        subscriptions_count = self.service.subscriptions_count
        if subscriptions_count == 0:
            return 0
        else:
            return self.cost_share()/self.service.subscriptions_count

    def value_share(self):
        return self.share*self.resource.get_prices()['unit_goods_value_share']

    def clean(self):
        """ Checks for cycles in service using services
        """
        start_resource = self.resource

        def crawl(service):
            for use in service.dependent_services.all():
                if use.service == start_resource:
                    raise ValidationError(
                        'Cycle detected in services using services')
                else:
                    crawl(use.service)

        crawl(self.service)
