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 core.models import Product, OrderLine, Coupon
from core.usps import USPSApi
from core import (
    DiscountValueType,
    VoucherType,
    TransactionStatus,
    OrderStatus,
    ShippingMethodType,
    ShippingService,
    ShippingContainer,
    CoffeeGrind
)

from .payments import CreateOrder

logger = logging.getLogger(__name__)


class Cart:
    def __init__(self, request):
        self.request = request
        self.session = request.session
        self.coupon_code = self.session.get('coupon_code')
        self.container = self.session.get('shipping_container')
        cart = self.session.get(settings.CART_SESSION_ID)
        if not cart:
            cart = self.session[settings.CART_SESSION_ID] = {}
        self.cart = cart

    def add(
        self, request, product, quantity=1, grind='', update_quantity=False
    ):
        product_id = str(product.id)
        if product_id not in self.cart:
            self.cart[product_id] = {
                'variations': {},
                'price': str(product.price)
            }
            self.cart[product_id]['variations'][grind] = {'quantity': 0}

        if update_quantity:
            self.cart[product_id]['variations'][grind]['quantity'] = quantity
        else:
            if not grind in self.cart[product_id]['variations']:
                # create it
                self.cart[product_id]['variations'][grind] = {'quantity': quantity}
            else:
                # add to it
                self.cart[product_id]['variations'][grind]['quantity'] += quantity
        if len(self) <= 20:
            self.save()
        else:
            messages.warning(request, "Cart is full: 20 items or less.")

    def save(self):
        self.session[settings.CART_SESSION_ID] = self.cart
        self.session.modified = True
        logger.info(f'\nCart:\n{self.cart}\n')

    def remove(self, product, grind):
        product_id = str(product.id)
        if product_id in self.cart:
            del self.cart[product_id]['variations'][grind]
            if not self.cart[product_id]['variations']:
                del self.cart[product_id]
            self.save()

    def __iter__(self):
        product_ids = self.cart.keys()
        products = Product.objects.filter(id__in=product_ids)
        for product in products:
            self.cart[str(product.id)]['product'] = product

        for item in self.cart.values():
            item['price'] = Decimal(item['price'])
            item['total_price'] = Decimal(sum(self.get_item_prices()))
            item['quantity'] = self.get_single_item_total_quantity(item)
            yield item

    def __len__(self):
        return sum(self.get_all_item_quantities())

    def get_all_item_quantities(self):
        for item in self.cart.values():
            yield sum([value['quantity'] for value in item['variations'].values()])

    def get_single_item_total_quantity(self, item):
        return sum([value['quantity'] for value in item['variations'].values()])

    def get_item_prices(self):
        for item in self.cart.values():
            yield Decimal(item['price']) * sum([value['quantity'] for value in item['variations'].values()])

    def get_total_price(self):
        return sum(self.get_item_prices())

    def get_total_weight(self):
        if len(self) > 0:
            for item in self:
                return item['product'].weight.value * sum(self.get_all_item_quantities())
        else:
            return 0

    def get_shipping_box(self, container=None):
        if container:
            return container

        if self.container:
            return self.container

        if len(self) > 6 and len(self) <= 10:
            return ShippingContainer.LG_FLAT_RATE_BOX
        elif len(self) > 3 and len(self) <= 6:
            return ShippingContainer.REGIONAL_RATE_BOX_B
        elif len(self) <= 3:
            return ShippingContainer.REGIONAL_RATE_BOX_A
        else:
            return ShippingContainer.VARIABLE

    def get_shipping_cost(self, container=None):
        if len(self) > 0 and self.session.get("shipping_address"):
            try:
                usps_rate_request = self.build_usps_rate_request(container)
            except TypeError as e:
                return Decimal('0.00')
            usps = USPSApi(settings.USPS_USER_ID, test=True)

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

            logger.info(validation.result)
            if 'Error' not in validation.result['RateV4Response']['Package']:
                rate = validation.result['RateV4Response']['Package']['Postage']['CommercialRate']
            else:
                logger.error("USPS Rate error")
                rate = '0.00'
            return Decimal(rate)
        else:
            return Decimal('0.00')

    def clear(self):
        del self.session[settings.CART_SESSION_ID]
        try:
            del self.session['coupon_code']
        except KeyError:
            pass
        self.session.modified = True

    def build_usps_rate_request(self, container=None):
        return \
            {
                'service': ShippingService.PRIORITY_COMMERCIAL,
                'zip_origination': settings.DEFAULT_ZIP_ORIGINATION,
                'zip_destination': f'{self.session.get("shipping_address")["postal_code"]}',
                'pounds': '0',
                'ounces': f'{self.get_total_weight()}',
                'container': f'{self.get_shipping_box(container)}',
                'width': '',
                'length': '',
                'height': '',
                'girth': '',
                'machinable': 'TRUE'
            }

    def build_order_params(self, container=None):
        return \
            {
                'items': self,
                'total_price': f'{self.get_total_price_after_discount()}',
                'item_total': f'{self.get_total_price()}',
                'discount': f'{self.get_discount()}',
                'shipping_price': f'{self.get_shipping_cost()}',
                'tax_total': '0',
                'shipping_method': 'US POSTAL SERVICE ' + (
                    container if container else ''
                ),
                'shipping_address': self.build_shipping_address(self.session.get('shipping_address')),
            }

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

    def build_bulk_list(self, order):
        bulk_list = []

        for item in self:
            for key, value in item['variations'].items():
                bulk_list.append(OrderLine(
                    order=order,
                    product=item['product'],
                    customer_note=next((v[1] for i, v in enumerate(CoffeeGrind.GRIND_CHOICES) if v[0] == key), None),
                    unit_price=item['price'],
                    quantity=value['quantity'],
                    tax_rate=2,
                ))

        return bulk_list

    def build_shipping_address(self, address):
        return \
            {
                'address_line_1': f'{address["street_address_1"]}',
                'address_line_2': f'{address["street_address_2"]}',
                'admin_area_2': f'{address["city"]}',
                'admin_area_1': f'{address["state"]}',
                'postal_code': f'{address["postal_code"]}',
                'country_code': 'US'
            }

    @property
    def coupon(self):
        if self.coupon_code:
            return Coupon.objects.get(code=self.coupon_code)
        return None

    def get_discount(self):
        if self.coupon:
            if self.coupon.discount_value_type == DiscountValueType.FIXED:
                return round(self.coupon.discount_value, 2)
            elif self.coupon.discount_value_type == DiscountValueType.PERCENTAGE:
                return round((self.coupon.discount_value / Decimal('100')) * self.get_total_price(), 2)
        return Decimal('0')

    def get_subtotal_price_after_discount(self):
        return round(self.get_total_price() - self.get_discount(), 2)

    def get_total_price_after_discount(self):
        return round(self.get_total_price() - self.get_discount() + self.get_shipping_cost(), 2)
