from __future__ import unicode_literals

from collections import namedtuple
from decimal import Decimal
from prices import Price
from uuid import uuid4

from django.conf import settings
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.utils.encoding import python_2_unicode_compatible, smart_str
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy
from django_prices.models import PriceField
from jsonfield import JSONField
from satchless.item import ItemLine, ItemList, partition
from django_prices.models import PriceField

from . import CartStatus, logger


CENTS = Decimal('0.01')
SimpleCart = namedtuple('SimpleCart', ('quantity', 'total', 'token'))


class ProductGroup(ItemList):
    def is_shipping_required(self):
        return any(p.is_shipping_required() for p in self)


class CartQueryset(models.QuerySet):

    def anonymous(self):
        return self.filter(user=None)

    def open(self):
        return self.filter(status=CartStatus.OPEN)

    def saved(self):
        return self.filter(status=CartStatus.SAVED)

    def waiting_for_payment(self):
        return self.filter(status=CartStatus.WAITING_FOR_PAYMENT)

    def checkout(self):
        return self.filter(status=CartStatus.CHECKOUT)

    def canceled(self):
        return self.filter(status=CartStatus.CANCELED)

    def for_display(self):
        return self.prefetch_related(
            'lines__variant__product__categories',
            'lines__variant__product__images',
            'lines__variant__product__product_class__product_attributes__values',  # noqa
            'lines__variant__product__product_class__variant_attributes__values',  # noqa
            'lines__variant__stock')


class Cart(models.Model):
    status = models.CharField(
        pgettext_lazy('Cart field', 'order status'),
        max_length=32, choices=CartStatus.CHOICES, default=CartStatus.OPEN)
    created = models.DateTimeField(
        pgettext_lazy('Cart field', 'created'), auto_now_add=True)
    last_status_change = models.DateTimeField(
        pgettext_lazy('Cart field', 'last status change'), auto_now_add=True)
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL, blank=True, null=True, related_name='carts',
        verbose_name=pgettext_lazy('Cart field', 'user'))
    email = models.EmailField(
        pgettext_lazy('Cart field', 'email'), blank=True, null=True)
    token = models.UUIDField(
        pgettext_lazy('Cart field', 'token'),
        primary_key=True, default=uuid4, editable=False)
    voucher = models.ForeignKey(
        'discount.Voucher', null=True, related_name='+',
        on_delete=models.SET_NULL,
        verbose_name=pgettext_lazy('Cart field', 'token'))
    checkout_data = JSONField(
        verbose_name=pgettext_lazy('Cart field', 'checkout data'), null=True,
        editable=False,)

    total = PriceField(
        pgettext_lazy('Cart field', 'total'),
        currency=settings.DEFAULT_CURRENCY, max_digits=12, decimal_places=2,
        default=0)
    quantity = models.PositiveIntegerField(
        pgettext_lazy('Cart field', 'quantity'), default=0)

    objects = CartQueryset.as_manager()

    class Meta:
        ordering = ('-last_status_change',)
        verbose_name = pgettext_lazy('Cart model', 'Cart')
        verbose_name_plural = pgettext_lazy('Cart model', 'Carts')

    def __init__(self, *args, **kwargs):
        self.discounts = kwargs.pop('discounts', None)
        super(Cart, self).__init__(*args, **kwargs)

    def update_quantity(self):
        total_lines = self.count()['total_quantity']
        if not total_lines:
            total_lines = 0
        self.quantity = total_lines
        self.save(update_fields=['quantity'])

    def change_status(self, status):
        if status not in dict(CartStatus.CHOICES):
            raise ValueError('Not expected status')
        if status != self.status:
            self.status = status
            self.last_status_change = now()
            self.save()

    def change_user(self, user):
        open_cart = Cart.get_user_open_cart(user)
        if open_cart is not None:
            open_cart.change_status(status=CartStatus.CANCELED)
        self.user = user
        self.save(update_fields=['user'])

    @staticmethod
    def get_user_open_cart(user):
        carts = user.carts.open()
        if len(carts) > 1:
            logger.warning('%s has more than one open basket', user)
            for cart in carts[1:]:
                cart.change_status(CartStatus.CANCELED)
        return carts.first()

    def is_shipping_required(self):
        return any(line.is_shipping_required() for line in self.lines.all())

    def __repr__(self):
        return 'Cart(quantity=%s)' % (self.quantity,)

    def __len__(self):
        return self.lines.count()

    def get_subtotal(self, item, **kwargs):
        return item.get_total(**kwargs)

    def get_total(self, **kwargs):
        subtotals = [self.get_subtotal(item, **kwargs)
                     for item in self.lines.all()]
        if not subtotals:
            raise AttributeError('Calling get_total() on an empty item set')
        zero = Price(0, currency=settings.DEFAULT_CURRENCY)
        return sum(subtotals, zero)

    def count(self):
        lines = self.lines.all()
        return lines.aggregate(total_quantity=models.Sum('quantity'))

    def clear(self):
        self.delete()

    def create_line(self, variant, quantity, data):
        line = self.lines.create(variant=variant, quantity=quantity,
                                 data=data or {})
        return line

    def get_line(self, variant, data=None):
        all_lines = self.lines.all()
        if data is None:
            data = {}
        line = [line for line in all_lines
                if line.variant_id == variant.id and line.data == data]
        if line:
            return line[0]

    def add(self, variant, quantity=1, bid_price=None, data=None, replace=False,
            check_quantity=True):
        cart_line, created = self.lines.get_or_create(
            variant=variant, bid_price=bid_price, defaults={'quantity': 0, 'data': data or {}})
        if replace:
            new_quantity = quantity
        else:
            new_quantity = cart_line.quantity + quantity

        if new_quantity < 0:
            raise ValueError('%r is not a valid quantity (results in %r)' % (
                quantity, new_quantity))

        if check_quantity:
            variant.check_quantity(new_quantity)

        cart_line.quantity = new_quantity

        if not cart_line.quantity:
            cart_line.delete()
        else:
            cart_line.save(update_fields=['quantity'])
        self.update_quantity()

    def partition(self):
        grouper = (
            lambda p: 'physical' if p.is_shipping_required() else 'digital')
        return partition(self.lines.all(), grouper, ProductGroup)


@python_2_unicode_compatible
class CartLine(models.Model, ItemLine):

    cart = models.ForeignKey(
        Cart, related_name='lines',
        verbose_name=pgettext_lazy('Cart line field', 'cart'))
    variant = models.ForeignKey(
        'product.ProductVariant', related_name='+',
        verbose_name=pgettext_lazy('Cart line field', 'product'))
    quantity = models.PositiveIntegerField(
        pgettext_lazy('Cart line field', 'quantity'),
        validators=[MinValueValidator(0), MaxValueValidator(999)])
    data = JSONField(
        blank=True, default={},
        verbose_name=pgettext_lazy('Cart line field', 'data'))

    bid_price = PriceField(
        pgettext_lazy('Cart line field', 'bid price'),
        currency=settings.DEFAULT_CURRENCY, max_digits=12, decimal_places=2,
        blank=True, null=True)

    class Meta:
        unique_together = ('cart', 'variant', 'data', 'bid_price')
        verbose_name = pgettext_lazy('Cart line model', 'Cart line')
        verbose_name_plural = pgettext_lazy('Cart line model', 'Cart lines')

    def __str__(self):
        return smart_str(self.variant)

    def __eq__(self, other):
        if not isinstance(other, CartLine):
            return NotImplemented

        return (self.variant == other.variant and
                self.quantity == other.quantity and
                self.data == other.data and
                self.bid_price == other.bid_price)

    def __ne__(self, other):
        return not self == other  # pragma: no cover

    def __repr__(self):
        return 'CartLine(variant=%r, quantity=%r, data=%r)' % (
            self.variant, self.quantity, self.data)

    def __getstate__(self):
        return self.variant, self.quantity, self.data, self.bid_price

    def __setstate__(self, data):
        self.variant, self.quantity, self.data, self.bid_price = data

    def get_total(self, **kwargs):
        amount = super(CartLine, self).get_total(**kwargs)
        return amount.quantize(CENTS)

    def get_quantity(self, **kwargs):
        return self.quantity

    def get_price_per_item(self, discounts=None, **kwargs):
        return self.bid_price or self.variant.get_price_per_item(discounts=discounts, **kwargs)
        # return self.variant.get_price_per_item(discounts=discounts, **kwargs)

    def is_shipping_required(self):
        return self.variant.is_shipping_required()
