import logging
from decimal import Decimal

from measurement.measures import Weight
from django.conf import settings
from django.contrib import messages
from django.shortcuts import redirect, reverse
from django.urls import reverse_lazy
from django.core.cache import cache
from django.db.models import OuterRef, Q, Subquery
from django.core.exceptions import ValidationError, ObjectDoesNotExist

from core.models import (
    ProductCategory, Product, ProductVariant, OrderLine, Coupon, ShippingRate,
    SiteSettings
)
from core.usps import USPSApi
from core.exceptions import USPSPostageError, ShippingAddressError
from core import (
    DiscountValueType,
    VoucherType,
    TransactionStatus,
    OrderStatus,
    ShippingService,
    ShippingProvider,
    ShippingContainer,
    CoffeeGrind
)
from core.shipping import build_usps_rate_request
from .forms import CartItemUpdateForm
from .payments import CreateOrder

logger = logging.getLogger(__name__)


class CartItem:
    update_form = CartItemUpdateForm
    order_line_class = OrderLine
    variant = None
    quantity = None
    options = None

    def __init__(self, item):
        try:
            self.variant = ProductVariant.objects.get(pk=item['variant_pk'])
        except ProductVariant.DoesNotExist:
            self.quantity = None
        else:
            self.quantity = item['quantity']
            self.options = item['options']

    def __iter__(self):
        yield ('name', f'{self.variant} {self.options_as_str()}')
        yield ('description', self.variant.product.subtitle)
        yield ('unit_amount', {
            'currency_code': settings.DEFAULT_CURRENCY,
            'value': str(self.variant.price),
        })
        yield ('quantity', str(self.quantity))

    def __str__(self):
        return f'{self.variant} [{self.quantity} × ${self.variant.price}]'

    def serialize(self):
        if self.variant is not None:
            return \
                {
                    'variant_pk': self.variant.pk,
                    'quantity': self.quantity,
                    'options': self.options
                }

    def deserialize(self, data):
        self.variant = ProductVariant.objects.get(data['variant_pk'])
        self.quantity = data['quantity']
        self.options = data['options']

    def options_as_str(self):
        options = [f'{key}: {value}' for key, value in self.options.items()]
        return '; '.join(options)

    def as_order_line(self, order):
        return self.order_line_class(
            order=order,
            variant=self.variant,
            customer_note=self.options_as_str(),
            unit_price=self.variant.price,
            quantity=self.quantity
        )

    def get_update_form(self, index):
        return self.update_form(initial={
            'item_index': index,
            'quantity': self.quantity
        })

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

    @property
    def total_weight(self):
        return Weight(lb=self.variant.weight.lb * self.quantity)


class Cart:
    item_class = CartItem
    items = []
    coupon = None
    request = None
    site_settings = None

    def __init__(self, request):
        self.request = request
        self.session = request.session
        self.site_settings = SiteSettings.load()
        if self.session.get('cart'):
            self.deserialize(self.session.get('cart'))
        self.save()

    def __iter__(self):
        for item in self.items:
            yield item

    def __len__(self):
        return sum([item.quantity for item in self])

    def serialize(self):
        return \
            {
                'coupon_code': self.coupon.code if self.coupon is not None else None,
                'items': [item.serialize() for item in self]
            }

    def deserialize(self, data):
        try:
            self.coupon = Coupon.objects.get(code=data.get('coupon_code'))
        except Coupon.DoesNotExist:
            self.coupon = None
        self.items = [self.item_class(item) for item in data['items'] if item is not None]

    def save(self):
        if self.validate():
            self.session['cart'] = self.serialize()
            self.session.modified = True

    def validate(self):
        self.check_item_checkout_limit_and_stock()
        if self.check_max_cart_quantity() and self.check_max_shipping_weight():
            return True
        return False

    def check_item_checkout_limit_and_stock(self):
        for item in self:
            if item.variant.track_inventory:
                if item.quantity > item.variant.stock:
                    if item.quantity > item.variant.product.checkout_limit:
                        messages.warning(self.request, 'Quantity exceeds checkout limit.')
                        item.quantity = item.variant.product.checkout_limit
                        continue
                    messages.warning(self.request, 'Quantity exceeds available stock.')
                    item.quantity = item.variant.stock
                elif item.quantity > item.variant.product.checkout_limit:
                    messages.warning(self.request, 'Quantity exceeds checkout limit.')
                    item.quantity = item.variant.product.checkout_limit

    def check_max_cart_quantity(self):
        if len(self) > self.site_settings.max_cart_quantity:
            messages.warning(self.request, 'Cart is full: 20 items or less.')
            return False
        return True

    def check_max_shipping_weight(self):
        if self.total_weight > self.site_settings.max_cart_weight:
            messages.warning(self.request, 'Weight exceeds shipping limit')
            return False
        return True

    def clear(self):
        del self.session['cart']
        self.session.modified = True

    def add_item(self, new_item):
        for item in self:
            if new_item.variant == item.variant:
                if new_item.options == item.options:
                    item.quantity += new_item.quantity
                    self.save()
                    return
                else:
                    continue
        self.items.append(new_item)
        self.save()

    def update_item_quantity(self, item_index, quantity):
        self.items[item_index].quantity = quantity
        self.save()

    def remove_item(self, index):
        self.items.pop(index)
        self.save()

    def add_coupon(self, coupon):
        # TODO: Apply coupon validity checks
        self.coupon = coupon
        self.save()

    def remove_coupon(self):
        if self.coupon is not None:
            del self.coupon
        self.save()

    def get_total_price_for_coupon_items(self):
        coupon_variants = self.coupon.variants.all()
        for item in self:
            if item.variant in coupon_variants:
                yield item.total_price

    def get_shipping_container_choices(self, total_weight=None):
        if total_weight is None:
            total_weight = self.total_weight

        is_selectable = Q(
            is_selectable=True
        )
        min_weight_matched = Q(
            min_order_weight__lte=total_weight) | Q(
            min_order_weight__isnull=True
        )
        max_weight_matched = Q(
            max_order_weight__gte=total_weight) | Q(
            max_order_weight__isnull=True
        )
        containers = ShippingRate.objects.filter(
            is_selectable & min_weight_matched & max_weight_matched
        )
        return containers

    def get_shipping_container(self):
        possible_containers = self.get_shipping_container_choices()
        if len(possible_containers) == 0:
            return self.site_settings.default_shipping_rate.container
        return possible_containers[0].container

    def get_shipping_price(self, container=None):
        if container is None:
            container = self.get_shipping_container()

        if not self.total_weight > Weight(lb=0):
            return Decimal('0.00')

        if len(self) > 0 and self.session.get('shipping_address'):
            usps_rate_request = build_usps_rate_request(
                str(self.total_weight.lb),
                container,
                str(self.session.get('shipping_address')['postal_code'])
            )

            usps = USPSApi(self.site_settings.usps_user_id, test=settings.DEBUG)

            try:
                validation = usps.get_rate(usps_rate_request)
            except ConnectionError as e:
                raise e(
                    'Could not connect to USPS, try again.'
                )

            logger.info(validation.result)
            try:
                postage = dict(
                    validation.result['RateV4Response']['Package']['Postage']
                )
            except KeyError:
                logger.warning(validation.result)
                raise USPSPostageError(
                    'Could not retrieve postage.'
                )

            if usps_rate_request['service'] == ShippingContainer.PRIORITY:
                shipping_cost = Decimal(postage['Rate'])
            elif usps_rate_request['service'] == ShippingContainer.PRIORITY_COMMERCIAL:
                shipping_cost = Decimal(postage['CommercialRate'])

            return shipping_cost
        else:
            raise ShippingAddressError(
                'Could not retrieve shipping address.'
            )

    def create_order(self):
        params = self.build_order_params()
        logger.info(f'\nParams: {params}\n')
        response = CreateOrder().create_order(params, debug=settings.DEBUG)
        return response

    def get_address_as_dict(self, address=None):
        if address is None:
            address = self.session.get('shipping_address')

        return \
            {
                'address_line_1': address.get('street_address_1', ''),
                'address_line_2': address.get('street_address_2', ''),
                'admin_area_2': address.get('city', ''),
                'admin_area_1': address.get('state', ''),
                'postal_code': address.get('postal_code', ''),
                'country_code': 'US'
            }

    def build_order_params(self):
        return \
            {
                'items': self.items,
                'total_price': self.total_price,
                'item_total': self.subtotal_price,
                'discount': self.discount_amount,
                'shipping_price': self.get_shipping_price(),
                'tax_total': '0.00',
                'shipping_method': 'US POSTAL SERVICE',
                'shipping_address': self.get_address_as_dict(),
            }

    def build_bulk_list(self, order):
        return [item.as_order_line(order) for item in self]

    @property
    def item_variant_pks(self):
        return [item.variant.pk for item in self]

    @property
    def subtotal_price(self):
        return sum([item.total_price for item in self])

    @property
    def discount_amount(self):
        if self.coupon is not None:
            if self.coupon.discount_value_type == DiscountValueType.FIXED:
                return self.coupon.discount_value
            elif self.coupon.discount_value_type == DiscountValueType.PERCENTAGE:
                if self.coupon.type == VoucherType.ENTIRE_ORDER:
                    return round((self.coupon.discount_value / Decimal('100.00')) * self.subtotal_price, 2)
                elif self.coupon.type == VoucherType.SPECIFIC_PRODUCT:
                    # Get the product in cart quantity
                    total_price = sum(self.get_total_price_for_coupon_items())
                    return round((self.coupon.discount_value / Decimal('100.00')) * total_price, 2)
        return Decimal('0.00')

    @property
    def subtotal_price_after_discount(self):
        return self.subtotal_price - self.discount_amount

    @property
    def total_price(self):
        return self.subtotal_price - self.discount_amount + self.get_shipping_price()

    @property
    def total_weight(self):
        return Weight(lb=sum([item.total_weight.lb for item in self]))
