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
from django.db.models import Sum

import markdown


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

    name = models.CharField('Nom', max_length=130)
    comment = models.TextField(
        'commentaire',
        blank=True, help_text="Texte brut ou markdown")
    comment_html = models.TextField(blank=True, editable=False)
    date = models.DateField(
        default=datetime.datetime.now,
        help_text="Date de création du document")
    type = models.CharField(max_length=10, choices=(
        (TYPE_PUBLIC, 'Rapport public'),
        (TYPE_DRAFT, 'Rapport brouillon'),
    ), help_text="Un rapport brouillon n'est pas visible publiquement")

    class Meta:
        ordering = ['-date']
        verbose_name = 'Rapport'

    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.type = Document.TYPE_DRAFT
            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

    def get_total_recuring_costs(self):
        return self.cost_set.aggregate(sum=Sum('price'))['sum'] or 0

    def get_total_goods(self):
        return self.good_set.aggregate(sum=Sum('price'))['sum'] or 0

    def compare(self, other_doc):
        """ Compare this document to another one

        :type other_doc: Document
        :rtype: dict
        """
        service_records = []
        for service in self.service_set.subscribables():
            other_service = Service.objects.similar_to(service).filter(
                document=other_doc).first()

            if other_service:
                deltas  = service.compare(other_service)
            else:
                deltas = {}

            service_records.append({
                'service': service,
                'other_service': other_service,
                'deltas': deltas,
            })

        comparison = {
            'services': service_records,
            'total_recuring_costs': self.get_total_recuring_costs() - other_doc.get_total_recuring_costs(),
            'total_goods': self.get_total_goods() - other_doc.get_total_goods(),
        }

        return comparison


class AbstractItem(models.Model):
    name = models.CharField('Nom', max_length=130)
    description = models.TextField('description', 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(
        'unité',
        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,
        help_text="unité de capacité (si applicable)",
    )
    total_capacity = models.FloatField(
        'Capacité totale',
        default=1,
        help_text="Laisser à 1 si non divisible")

    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("Coût mensuel")

    def get_use_class(self):
        return CostUse

    class Meta:
        verbose_name = 'Coût mensuel'


class GoodQuerySet(models.QuerySet):
    def monthly_provision_total(self):
        # FIXME: could be optimized easily
        return sum([i.monthly_provision() for i in self.all()])


class Good(AbstractResource):
    """ A good, which replacement is provisioned
    """
    price = models.FloatField("Prix d'achat")
    provisioning_duration = models.DurationField(
        "Durée d'amortissement",
        choices=settings.PROVISIONING_DURATIONS)

    objects = GoodQuerySet.as_manager()

    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 = "Matériel ou Frais d'accès"
        verbose_name_plural = "Matériels ou Frais d'accès"


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

    class Meta:
        abstract = True

    def __str__(self):
        return str(self.resource)

    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)

    class Meta:
        verbose_name = 'Coût mensuel associé'
        verbose_name_plural = 'Coûts mensuels associés'

    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)

    class Meta:
        verbose_name = "Matériel ou frais d'accès utilisé"
        verbose_name_plural = "Matériels et frais d'accès utilisés"

    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 ServiceQuerySet(models.QuerySet):
    def subscribables(self):
        return self.exclude(internal=True)

    def similar_to(self, service):
        return self.filter(name=service.name).exclude(pk=service.pk)

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

    (considered monthly)
    """
    costs = models.ManyToManyField(
        Cost,
        through=CostUse,
        related_name='using_services',
        verbose_name='coûts mensuels associés')
    goods = models.ManyToManyField(
        Good,
        through=GoodUse,
        related_name='using_services',
        verbose_name="matériels et frais d'accès utilisés")

    subscriptions_count = models.PositiveIntegerField(
        "Nombre d'abonnements",
        default=0)
    reusable = models.BooleanField(
        "Ré-utilisable par d'autres services",
        default=False,
        help_text="Peut-être utilisé par d'autres services")
    internal = models.BooleanField(
        "Service interne",
        default=False,
        help_text="Ne peut être vendu tel quel, n'apparaît pas dans la liste des services"
    )

    objects =  ServiceQuerySet.as_manager()


    @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
        unit_staggered_goods_share = \
            unit_goods_value_share/settings.SETUP_COST_STAGGERING_MONTHS
        return {
            'total_recurring_price': total_recurring_price,
            'total_goods_value_share': total_goods_value_share,
            'unit_goods_value_share': unit_goods_value_share,
            'unit_recurring_price': unit_recurring_price,
            'unit_staggered_goods_share': unit_staggered_goods_share,
            'unit_consolidated_cost': (
                unit_staggered_goods_share + unit_recurring_price),
        }

    def compare(self, other):
        """ Compares figures between two services

        Typically, same service type, but on two different reports.

        :type other: Service
        :rtype: dict
        :returns: dict of deltas (from other to self).
        """

        self_prices = self.get_prices()
        other_prices = other.get_prices()

        comparated_prices = (
            'total_recurring_price', 'total_goods_value_share',
            'unit_goods_value_share', 'unit_recurring_price',
            'unit_staggered_goods_share', 'unit_consolidated_cost',
        )

        comparated_vals = {
            'subscriptions_count': self.subscriptions_count - other.subscriptions_count,
        }
        for i in comparated_prices:
            comparated_vals[i] = self_prices[i] - other_prices[i]

        return comparated_vals


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):
    class Meta:
        verbose_name = 'service utilisé'
        verbose_name_plural = 'services utilisés'

    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)