import itertools
import uuid

from django.core.exceptions import ObjectDoesNotExist
from django.contrib.sites.models import Site
from django.contrib.sites.managers import CurrentSiteManager
from django.db import models
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from django.urls import reverse
from allauth.account.signals import user_logged_in

from vendor.models.utils import set_default_site_id
from vendor.config import DEFAULT_CURRENCY
from vendor.utils import get_site_from_request

from .base import CreateUpdateModelBase
from .choice import CURRENCY_CHOICES, TermType
from .offer import Offer


#####################
# INVOICE
#####################
class Invoice(CreateUpdateModelBase):
    '''
    An invoice starts off as a Cart until it is puchased, then it becomes an Invoice.
    '''
    class InvoiceStatus(models.IntegerChoices):
        CART = 0, _("Cart")               # total = subtotal = sum(OrderItems.Offer.Price + Product.TaxClassifier). Avalara
        CHECKOUT = 10, _("Checkout")      # total = subtotal + shipping + Tax against Addrr if any.
        QUEUED = 20, _("Queued")          # Queued to for Payment Processor.
        PROCESSING = 30, _("Processing")  # Payment Processor update, start of payment.
        FAILED = 40, _("Failed")          # Payment Processor Failed Transaction.
        COMPLETE = 50, _("Complete")      # Payment Processor Completed Transaction.
        REFUNDED = 60, _("Refunded")      # Invoice Refunded to client.

    uuid = models.UUIDField(_("UUID"), default=uuid.uuid4, editable=False, unique=True)
    profile = models.ForeignKey("vendor.CustomerProfile", verbose_name=_("Customer Profile"), null=True, on_delete=models.CASCADE, related_name="invoices")     # TODO: [GK-3029] remove null=True.  This should not be allowed.
    site = models.ForeignKey(Site, verbose_name=_("Site"), on_delete=models.CASCADE, default=set_default_site_id, related_name="invoices")                      # For multi-site support
    status = models.IntegerField(_("Status"), choices=InvoiceStatus.choices, default=InvoiceStatus.CART)
    customer_notes = models.JSONField(_("Customer Notes"), default=dict, blank=True, null=True)
    vendor_notes = models.JSONField(_("Vendor Notes"), default=dict, blank=True, null=True)
    ordered_date = models.DateTimeField(_("Ordered Date"), blank=True, null=True)               # When was the purchase made?
    subtotal = models.FloatField(default=0.0)
    tax = models.FloatField(blank=True, null=True)                              # Set on checkout
    shipping = models.FloatField(blank=True, null=True)                         # Set on checkout
    total = models.FloatField(blank=True, null=True)                            # Set on purchase
    currency = models.CharField(_("Currency"), max_length=4, choices=CURRENCY_CHOICES, default=DEFAULT_CURRENCY)      # User's default currency
    shipping_address = models.ForeignKey("vendor.Address", verbose_name=_("Shipping Address"), on_delete=models.CASCADE, blank=True, null=True)
    # paid = models.BooleanField(_("Paid"))                 # May be Useful for quick filtering on invoices that are outstanding
    # settle_date = models.DateTimeField(_("Settle Date"))

    objects = models.Manager()
    on_site = CurrentSiteManager()

    class Meta:
        verbose_name = "Invoice"
        verbose_name_plural = "Invoices"
        ordering = ['-ordered_date', '-updated']             # TODO: [GK-2518] change to use ordered_date.  Invoice ordered_date needs to be updated on successful purchase by the PaymentProcessor.

        permissions = (
            ('can_view_site_purchases', 'Can view Site Purchases'),
            ('can_refund_purchase', 'Can refund Purchase'),
        )

    def __str__(self):
        if not self.profile.user:   # Can this ever even happen?
            return "New Invoice"
        return f"{self.profile.user.username} - {self.uuid}"

    def get_invoice_display(self):
        return _(f"{self.profile.user.username} Invoice ({self.created:%Y-%m-%d %H:%M})")

    def add_offer(self, offer, quantity=1):

        order_item, created = self.order_items.get_or_create(offer=offer)
        # make sure the invoice pk is also in the OriderItem
        if not created and order_item.offer.allow_multiple:
            order_item.quantity += quantity
            order_item.save()

        self.update_totals()
        self.save()
        return order_item

    def remove_offer(self, offer, clear=False):
        try:
            order_item = self.order_items.get(offer=offer)      # Get the order item if it's present
        except ObjectDoesNotExist:
            return 0

        order_item.quantity -= 1

        if order_item.quantity == 0 or clear:
            order_item.delete()
        else:
            order_item.save()

        self.update_totals()
        self.save()
        return order_item

    def swap_offer(self, existing_offer, new_offer):
        """
        Functions swaps offers that have the same linked product. It will not remove bundle offers
        that also have shared product with the new offer. The function comes in handy to swap
        an offer that has the normal price with one that has a discount price or terms.
        """
        if not existing_offer.products.filter(pk__in=[offer.pk for offer in new_offer.products.all()]).exists():
            return None

        if self.order_items.filter(offer=existing_offer).exists():
            order_items_same_product = self.order_items.filter(offer=existing_offer).exclude(offer__bundle=True)
            for order_item in order_items_same_product:
                self.remove_offer(order_item.offer, clear=True)

        self.add_offer(new_offer)
        self.update_totals()
        self.save()

    def calculate_shipping(self):
        '''
        Based on the Shipping Address
        '''
        self.shipping = 0

    def calculate_tax(self):
        '''
        Extendable
        '''
        self.tax = 0

    def update_totals(self):
        self.subtotal = sum([item.total for item in self.order_items.all() ])

        self.calculate_shipping()
        self.calculate_tax()
        self.total = self.subtotal + self.tax + self.shipping

    def get_payment_billing_address(self):
        if not self.payments.filter(success=True).first().billing_address:
            return ""
        return self.payments.filter(success=True).first().billing_address.get_address_display()

    # def get_absolute_url(self):       # TODO: [GK-3031] add and user view for invoice detail
    #     """
    #     This is the url to the detail page for the Invoice
    #     """
    #     return reverse('vendor:invoice-detail', kwargs={'uuid': self.uuid})

    def get_absolute_management_url(self):
        """
        This is the url to the management's detail page for the Invoice
        """
        return reverse('vendor_admin:manager-order-detail', kwargs={'uuid': self.uuid})

    def get_recurring_order_items(self):
        """
        Gets the recurring order items in the invoice
        """
        return self.order_items.filter(offer__terms__lt=TermType.PERPETUAL)

    def get_recurring_total(self):
        """
        Gets the total price for all recurring order items in the invoice
        """
        return sum([ order_item.total for order_item in self.order_items.filter(offer__terms__lt=TermType.PERPETUAL)])

    def get_one_time_transaction_order_items(self):
        """
        Gets one time transation order items in the invoice
        """
        return self.order_items.filter(offer__terms__gte=TermType.PERPETUAL)

    def get_one_time_transaction_total(self):
        """
        Gets the total price for order items that will be purchased on a single transation.
        """
        return sum([ order_item.total for order_item in self.order_items.filter(offer__terms__gte=TermType.PERPETUAL)])

    def empty_cart(self):
        """
        Remove any offer/order_item if the invoice is in Cart State.
        """
        offers = []
        offers = list(itertools.chain.from_iterable([ [order_item.offer] * order_item.quantity for order_item in self.order_items.all()]))
        for offer in offers:
            self.remove_offer(offer)


class OrderItem(CreateUpdateModelBase):
    '''
    A link for each item to a user after it's been purchased
    '''
    invoice = models.ForeignKey("vendor.Invoice", verbose_name=_("Invoice"), on_delete=models.CASCADE, related_name="order_items")
    offer = models.ForeignKey("vendor.Offer", verbose_name=_("Offer"), on_delete=models.CASCADE, related_name="order_items")
    quantity = models.IntegerField(_("Quantity"), default=1)

    class Meta:
        verbose_name = "Order Item"
        verbose_name_plural = "Order Items"

    def __str__(self):
        return f"{self.offer} - {self.invoice.uuid}"

    @property
    def total(self):
        return self.quantity * self.price

    @property
    def price(self):
        return self.offer.current_price()

    @property
    def name(self):
        return self.offer.name

    def get_total_display(self):
        if not self.total:
            return "0.00"

        return f'{self.total:2}'


##########
# Signals
##########
@receiver(user_logged_in)
def convert_session_cart_to_invoice(sender, request, **kwargs):
    if 'session_cart' in request.session:
        profile, created = request.user.customer_profile.get_or_create(site=get_site_from_request(request))
        cart = profile.get_cart()

        for offer_key in request.session['session_cart'].keys():
            cart.add_offer(Offer.objects.get(pk=offer_key), quantity=request.session['session_cart'][offer_key]['quantity'])

        del(request.session['session_cart'])
