import logging
import requests
import json
import stripe
from decimal import Decimal
from django.conf import settings
from django.utils import timezone
from django.shortcuts import render, reverse, redirect, get_object_or_404
from django.urls import reverse_lazy
from django.core.mail import EmailMessage
from django.core.cache import cache
from django.contrib.sites.models import Site
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.http import JsonResponse, HttpResponseRedirect
from django.views.generic.base import View, RedirectView, TemplateView
from django.views.generic.edit import (
    FormView, CreateView, UpdateView, DeleteView, FormMixin
)
from django.views.generic.detail import DetailView, SingleObjectMixin
from django.views.generic.list import ListView
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.contrib import messages
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from django.forms.models import model_to_dict
from django.db.models import (
    Exists, OuterRef, Prefetch, Subquery, Count, Sum, Avg, F, Q, Value
)
from measurement.measures import Weight
from measurement.utils import guess
from paypalcheckoutsdk.orders import OrdersCreateRequest, OrdersCaptureRequest
from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment
from moneyed import Money, USD

from accounts.models import User, Address
from accounts.utils import get_or_create_customer
from accounts.forms import (
    AddressForm as AccountAddressForm, CustomerUpdateForm
)
from core.models import (
    ProductCategory, Product, ProductVariant, ProductOption,
    Order, Transaction, OrderLine, Coupon, ShippingRate,
    Subscription, SiteSettings
)
from core.forms import ShippingRateForm
from core.shipping import get_shipping_cost
from core import OrderStatus, ShippingContainer

from .forms import (
    AddToCartForm, CartItemUpdateForm, OrderCreateForm,
    AddressForm, CouponApplyForm, CheckoutShippingForm,
    SubscriptionForm
)
from .cart import CartItem, Cart
from .payments import CaptureOrder

logger = logging.getLogger(__name__)
stripe.api_key = settings.STRIPE_API_KEY


class CartView(FormView):
    template_name = 'storefront/cart_detail.html'
    form_class = CartItemUpdateForm

    def get_success_url(self):
        return reverse('storefront:cart-detail')

    def post(self, request, *args, **kwargs):
        cart = Cart(request)
        form = self.get_form()
        if form.is_valid():
            cart.update_item_quantity(
                form.cleaned_data['item_index'],
                form.cleaned_data['quantity']
            )
            return self.form_valid(form)
        else:
            return self.form_invalid(form)

    def form_valid(self, form):
        return super().form_valid(form)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['coupon_apply_form'] = CouponApplyForm()
        return context


class CartAddProductView(SingleObjectMixin, FormView):
    model = Product
    form_class = AddToCartForm
    http_method_names = ['post']

    def get_success_url(self):
        return reverse('storefront:cart-detail')

    def get_form(self, form_class=None):
        variants = self.get_object().variants.filter(
            Q(track_inventory=False) | Q(
                track_inventory=True,
                stock__gt=0
            )
        )
        options = ProductOption.objects.filter(products__pk=self.get_object().pk)
        if form_class is None:
            form_class = self.get_form_class()
        return form_class(variants, options, **self.get_form_kwargs())

    def post(self, request, *args, **kwargs):
        cart = Cart(request)
        form = self.get_form()
        if form.is_valid():
            cleaned_data = form.cleaned_data
            cart.add_item(
                CartItem({
                    'variant_pk': cleaned_data.pop('variant').pk,
                    'quantity': cleaned_data.pop('quantity'),
                    'options': cleaned_data
                })
            )
            return self.form_valid(form)
        else:
            return self.form_invalid(form)


class CartItemUpdateView(FormView):
    form_class = CartItemUpdateForm
    http_method_names = ['post']

    def get_success_url(self):
        return reverse('storefront:cart-detail')

    def post(self, request, *args, **kwargs):
        cart = Cart(request)
        form = self.get_form()
        if form.is_valid():
            cart.update_item_quantity(
                form.cleaned_data['item_index'],
                form.cleaned_data['quantity']
            )
            return self.form_valid(form)
        else:
            return self.form_invalid(form)

    def form_valid(self, form):
        return super().form_valid(form)


def cart_remove_product_view(request, pk):
    cart = Cart(request)
    cart.remove_item(pk)
    return redirect('storefront:cart-detail')


class CouponApplyView(FormView):
    form_class = CouponApplyForm
    success_url = reverse_lazy('storefront:cart-detail')
    http_method_names = ['post']

    def form_valid(self, form):
        today = timezone.localtime(timezone.now()).date()
        try:
            coupon = Coupon.objects.get(code=form.cleaned_data['code'])
        except Coupon.DoesNotExist:
            messages.warning(self.request, 'Coupon does not exist.')
        else:
            if coupon.is_valid:
                cart = Cart(self.request)
                cart.add_coupon(coupon)
            else:
                messages.warning(self.request, 'Coupon is invalid.')
        return super().form_valid(form)


class ProductCategoryDetailView(DetailView):
    model = ProductCategory
    template_name = 'storefront/category_detail.html'
    context_object_name = 'category'

    def get_queryset(self):
        object_list = ProductCategory.objects.prefetch_related(
            Prefetch(
                'product_set',
                queryset=Product.objects.filter(
                    Q(visible_in_listings=True),
                    Q(variants__visible_in_listings=True,
                      variants__track_inventory=False) |
                    Q(variants__visible_in_listings=True,
                      variants__track_inventory=True,
                      variants__stock__gt=0)
                ).prefetch_related(
                    Prefetch(
                        'variants',
                        queryset=ProductVariant.objects.filter(
                            visible_in_listings=True
                        ).order_by('sorting', 'weight')
                    )
                ).distinct()
            )
        )
        return object_list


class ProductListView(ListView):
    model = Product
    template_name = 'storefront/product_list.html'
    ordering = ['category', 'sorting']

    queryset = Product.objects.filter(
        Q(visible_in_listings=True, category__main_category=True),
        Q(variants__visible_in_listings=True,
          variants__track_inventory=False) |
        Q(variants__visible_in_listings=True,
          variants__track_inventory=True,
          variants__stock__gt=0)
    ).prefetch_related(
        Prefetch(
            'variants',
            queryset=ProductVariant.objects.filter(
                visible_in_listings=True
            ).order_by('sorting', 'weight')
        )
    ).distinct()


class ProductDetailView(FormMixin, DetailView):
    model = Product
    template_name = 'storefront/product_detail.html'
    form_class = AddToCartForm

    def get_form(self, form_class=None):
        variants = self.object.variants.filter(
            Q(track_inventory=False, visible_in_listings=True) | Q(
                visible_in_listings=True,
                track_inventory=True,
                stock__gt=0
            )
        ).order_by('sorting', 'name')
        options = ProductOption.objects.filter(products__pk=self.object.pk)
        if form_class is None:
            form_class = self.get_form_class()
        return form_class(variants, options, **self.get_form_kwargs())


class CheckoutAddressView(FormView):
    template_name = 'storefront/checkout_address.html'
    form_class = AddressForm
    success_url = reverse_lazy('storefront:order-create')

    def get_initial(self):
        user = self.request.user
        initial = None
        if user.is_authenticated and user.default_shipping_address:
            address = user.default_shipping_address
            initial = {
                'full_name': address.first_name + ' ' + address.last_name,
                'email': user.email,
                'street_address_1': address.street_address_1,
                'street_address_2': address.street_address_2,
                'city': address.city,
                'state': address.state,
                'postal_code': address.postal_code
            }
        elif self.request.session.get('shipping_address'):
            address = self.request.session.get('shipping_address')
            initial = {
                'full_name': address['first_name'] + ' ' + address['last_name'],
                'email': address['email'],
                'street_address_1': address['street_address_1'],
                'street_address_2': address['street_address_2'],
                'city': address['city'],
                'state': address['state'],
                'postal_code': address['postal_code']
            }
        return initial

    def form_valid(self, form):
        # save address data to session
        cleaned_data = form.cleaned_data
        first_name, last_name = form.process_full_name(
            cleaned_data.get('full_name')
        )
        address = {
            'first_name': first_name,
            'last_name': last_name,
            'email': cleaned_data['email'],
            'street_address_1': cleaned_data['street_address_1'],
            'street_address_2': cleaned_data['street_address_2'],
            'city': cleaned_data['city'],
            'state': cleaned_data['state'],
            'postal_code': cleaned_data['postal_code']
        }
        self.request.session['shipping_address'] = address
        return super().form_valid(form)


class CheckoutShippingView(FormView):
    template_name = 'storefront/checkout_shipping_form.html'
    form_class = CheckoutShippingForm
    success_url = reverse_lazy('storefront:order-create')
    containers = None

    def get_containers(self, request):
        if self.containers is None:
            cart = Cart(request)
            self.containers = cart.get_shipping_container()
        return self.containers

    def get(self, request, *args, **kwargs):
        if not self.request.session.get('shipping_address'):
            messages.warning(request, 'Please add a shipping address.')
            return HttpResponseRedirect(
                reverse('storefront:checkout-address')
            )
        site_settings = cache.get('SiteSettings')
        cart = Cart(self.request)
        if len(self.get_containers(request)) == 0:
            self.request.session['shipping_container'] = site_settings.default_shipping_rate
            return HttpResponseRedirect(self.success_url)
        elif len(self.get_containers(request)) == 1:
            self.request.session['shipping_container'] = self.get_containers(request)[0]
            return HttpResponseRedirect(self.success_url)
        return super().get(request, *args, **kwargs)

    def get_form(self, form_class=None):
        cart = Cart(self.request)
        for container in self.get_containers(self.request):
            container.s_cost = cart.get_shipping_price(container.container)
        if form_class is None:
            form_class = self.get_form_class()
        return form_class(self.get_containers(self.request), **self.get_form_kwargs())

    def form_valid(self, form):
        shipping_container = ShippingRate.objects.get(
            pk=form.cleaned_data.get('shipping_method')
        )
        self.request.session['shipping_container'] = shipping_container
        return super().form_valid(form)


class OrderCreateView(CreateView):
    model = Order
    template_name = 'storefront/order_form.html'
    form_class = OrderCreateForm
    success_url = reverse_lazy('storefront:payment-done')

    def get(self, request, *args, **kwargs):
        if not self.request.session.get('shipping_address'):
            messages.warning(request, 'Please add a shipping address.')
            return HttpResponseRedirect(
                reverse('storefront:checkout-address')
            )

        cart = Cart(request)

        if cart.coupon is not None:
            try:
                user = User.objects.get(
                    email=request.session.get('shipping_address').get('email')
                )
            except User.DoesNotExist:
                user = None
            if user in cart.coupon.users.all():
                cart.remove_coupon()
                messages.warning(request, 'Coupon already used.')
        return super().get(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['shipping_address'] = self.request.session.get('shipping_address')
        context['PAYPAL_CLIENT_ID'] = settings.PAYPAL_CLIENT_ID
        return context

    def form_valid(self, form):
        cart = Cart(self.request)
        form.instance.subtotal_amount = cart.subtotal_price_after_discount
        form.instance.coupon = cart.coupon
        form.instance.coupon_amount = cart.discount_amount
        form.instance.total_amount = cart.total_price
        form.instance.weight = cart.total_weight
        shipping_container = cart.get_shipping_container()
        form.instance.shipping_total = cart.get_shipping_price(shipping_container)
        shipping_address = self.request.session.get('shipping_address')
        form.instance.customer, form.instance.shipping_address = get_or_create_customer(self.request, shipping_address)
        form.instance.status = OrderStatus.DRAFT
        self.object = form.save()
        bulk_list = cart.build_bulk_list(self.object)
        objs = OrderLine.objects.bulk_create(bulk_list)

        response = cart.create_order()
        data = response.result.__dict__['_dict']

        self.request.session['order_id'] = self.object.pk

        return JsonResponse(data)


@csrf_exempt
@require_POST
def paypal_order_transaction_capture(request, transaction_id):
    if request.method == "POST":
        data = CaptureOrder().capture_order(transaction_id)
        cart = Cart(request)
        order = Order.objects.get(pk=request.session.get('order_id'))
        order.status = OrderStatus.UNFULFILLED
        order.minus_stock()
        try:
            coupon = Coupon.objects.get(
                    code=request.session.get('coupon_code')
                )
        except ObjectDoesNotExist:
            coupon = None

        if coupon:
            order.coupon = coupon
            coupon.users.add(order.customer)
        order.save()
        transaction = Transaction.objects.get(order=order)
        transaction.paypal_id = data['purchase_units'][0]['payments']['captures'][0]['id']
        transaction.status = data['status']
        transaction.save()
        cart.clear()
        logger.debug(f'\nPayPal Response data: {data}\n')
        return JsonResponse(data)
    else:
        return JsonResponse({'details': 'invalid request'})


class PaymentDoneView(TemplateView):
    template_name = 'storefront/payment_done.html'


class PaymentCanceledView(TemplateView):
    template_name = 'storefront/payment_canceled.html'


class CustomerDetailView(UserPassesTestMixin, LoginRequiredMixin, DetailView):
    model = User
    template_name = 'storefront/customer_detail.html'
    context_object_name = 'customer'
    permission_denied_message = 'Not authorized.'
    raise_exception = True

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['order_list'] = Order.objects.without_drafts().filter(
            customer=self.object
        ).prefetch_related('lines')
        subscriptions = []
        if self.object.stripe_id is not None:
            subscriptions = stripe.Subscription.list(customer=self.object.stripe_id)['data']
        context['subscriptions'] = subscriptions
        return context

    def test_func(self):
        return self.request.user.pk == self.get_object().pk


class CustomerUpdateView(UserPassesTestMixin, LoginRequiredMixin, UpdateView):
    model = User
    template_name = 'storefront/customer_form.html'
    context_object_name = 'customer'
    form_class = CustomerUpdateForm
    permission_denied_message = 'Not authorized.'
    raise_exception = True

    def test_func(self):
        return self.request.user.pk == self.get_object().pk

    def get_success_url(self):
        return reverse(
            'storefront:customer-detail', kwargs={'pk': self.object.pk}
        )


class OrderDetailView(UserPassesTestMixin, LoginRequiredMixin, DetailView):
    model = Order
    template_name = 'storefront/order_detail.html'
    pk_url_kwarg = 'order_pk'
    permission_denied_message = 'Not authorized.'
    raise_exception = True

    def test_func(self):
        return self.request.user.pk == self.get_object().customer.pk

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['customer'] = User.objects.get(pk=self.kwargs['pk'])
        return context


class CustomerAddressCreateView(
    UserPassesTestMixin, LoginRequiredMixin, CreateView
):
    model = Address
    template_name = 'storefront/address_create_form.html'
    form_class = AccountAddressForm
    permission_denied_message = 'Not authorized.'
    raise_exception = True

    def test_func(self):
        return self.request.user.pk == self.kwargs['pk']

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['customer'] = User.objects.get(pk=self.kwargs['pk'])
        return context

    def form_valid(self, form):
        customer = User.objects.get(pk=self.kwargs['pk'])
        self.object = form.save()
        customer.addresses.add(self.object)

        return super().form_valid(form)

    def get_success_url(self):
        return reverse(
            'storefront:customer-detail', kwargs={'pk': self.kwargs['pk']}
        )


class CustomerAddressUpdateView(
    UserPassesTestMixin, LoginRequiredMixin, UpdateView
):
    model = Address
    pk_url_kwarg = 'address_pk'
    template_name = 'storefront/address_form.html'
    form_class = AccountAddressForm
    permission_denied_message = 'Not authorized.'
    raise_exception = True

    def test_func(self):
        return self.request.user.pk == self.kwargs['pk']

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['customer'] = User.objects.get(pk=self.kwargs['pk'])
        return context

    def get_success_url(self):
        return reverse('storefront:customer-detail', kwargs={'pk': self.kwargs['pk']})


class AboutView(TemplateView):
    template_name = 'storefront/about.html'


class FairTradeView(TemplateView):
    template_name = 'storefront/fairtrade.html'


class ReviewListView(TemplateView):
    template_name = 'storefront/reviews.html'


class SubscriptionAdView(TemplateView):
    template_name = 'storefront/subscription/ad.html'


class SubscriptionFormView(FormView):
    template_name = 'storefront/subscription/form.html'
    form_class = SubscriptionForm
    success_url = reverse_lazy('storefront:subscription-address')

    def get_stripe_products(self):
        # id, name
        product_list = [{
            'id': product['id'],
            'name': product['name'],
            'created': product['created'],
            'weight_per_item': product['metadata']['weight_per_item'],
            'cost': product['metadata']['cost'],
            'prices': []
        } for product in stripe.Product.list(active=True)]

        # id, product, recurring.interval_count, recurring.interval, unit_amount
        price_list = [{
            'id': price['id'],
            'product': price['product'],
            'interval_count': price.recurring.interval_count,
            'interval': price.recurring.interval,
            'unit_amount': price['unit_amount']
        } for price in stripe.Price.list(active=True)]

        for prod in product_list:
            prod['prices'] = list(filter(
                lambda p: True if p['product'] == prod['id'] else False,
                price_list
            ))
        return sorted(product_list, key=lambda p: p['created'])

    def get_context_data(self, *args, **kwargs):
        context = super().get_context_data(*args, **kwargs)
        context['stripe_products'] = self.get_stripe_products()
        context['product_list'] = Product.objects.filter(
            visible_in_listings=True,
            category__name='Coffee'
        )
        return context

    def form_valid(self, form):
        self.request.session['subscription'] = {
            'items': [{
                'price': form.cleaned_data['stripe_price_id'],
                'quantity': form.cleaned_data['total_quantity']
            }],
            'metadata': {
                'grind': form.cleaned_data['grind'],
                'total_weight': form.cleaned_data['total_weight'],
                'products_and_quantities': json.loads(form.cleaned_data['products_and_quantities'])
            }
        }
        return super().form_valid(form)


class SubscriptionAddAddressView(CheckoutAddressView):
    template_name = 'storefront/subscription/address.html'
    success_url = reverse_lazy('storefront:subscription-create')


class SubscriptionCreateView(SuccessMessageMixin, CreateView):
    model = Subscription
    success_message = 'Subscription created.'
    template_name = 'storefront/subscription/create_form.html'
    fields = []

    def get(self, request, *args, **kwargs):
        if not self.request.session.get('subscription'):
            return HttpResponseRedirect(reverse('storefront:subscription-form'))
        return super().get(request, *args, **kwargs)

    def get_item_list(self):
        item_list = [{
            'product': Product.objects.get(pk=item['pk']),
            'quantity': item['quantity']
        } for item in self.request.session['subscription']['metadata']['products_and_quantities']]
        return item_list

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        subscription = self.request.session['subscription']
        metadata = subscription['metadata']
        price = stripe.Price.retrieve(
            subscription['items'][0].get('price'),
            expand=['product']
        )
        shipping_address = self.request.session.get('shipping_address')
        weight, unit = metadata['total_weight'].split(':')
        total_weight = guess(float(weight), unit, measures=[Weight])
        subtotal_price = (Decimal(price.unit_amount) * subscription['items'][0]['quantity']) / 100
        shipping_cost = get_shipping_cost(
            total_weight,
            shipping_address['postal_code']
        )
        total_price = subtotal_price + shipping_cost
        context['sub_cart'] = {
            'items': self.get_item_list(),
            'size': price.product.name,
            'grind': metadata['grind'],
            'schedule': f'Every {price.recurring.interval_count} / {price.recurring.interval}',
            'subtotal_price': Money(subtotal_price, USD),
            'shipping_cost': shipping_cost,
            'total_price': total_price,
            'total_weight': guess(float(weight), unit, measures=[Weight])
        }
        context['shipping_address'] = shipping_address
        return context

    def get_line_items(self):
        line_items = self.object.items
        recurring = stripe.Price.retrieve(
            line_items[0].get('price')
        ).get('recurring')
        shipping_cost = get_shipping_cost(
            self.object.total_weight,
            self.object.shipping_address.postal_code
        ) * 100
        line_items.append({
            'price_data': {
                'currency': settings.DEFAULT_CURRENCY.lower(),
                'unit_amount': int(shipping_cost),
                'product_data': {
                    'name': 'Shipping'
                },
                'recurring': {
                    'interval': recurring.interval,
                    'interval_count': recurring.interval_count
                }
            },
            'quantity': 1
        })
        return line_items

    def get_success_url(self):
        session = stripe.checkout.Session.create(
            customer=self.object.customer.get_or_create_stripe_id(),
            success_url='http://' + Site.objects.get_current().domain + reverse(
                'storefront:subscription-done'
            ) + '?session_id={CHECKOUT_SESSION_ID}',
            cancel_url='http://' + Site.objects.get_current().domain + reverse(
                'storefront:subscription-create'),
            mode='subscription',
            line_items=self.get_line_items(),
            subscription_data={'metadata': self.object.format_metadata()}
        )
        return session.url

    def form_valid(self, form):
        shipping_address = self.request.session.get('shipping_address')
        subscription = self.request.session.get('subscription')
        form.instance.customer, form.instance.shipping_address = get_or_create_customer(self.request, shipping_address)
        weight, unit = subscription['metadata']['total_weight'].split(':')
        form.instance.total_weight = guess(
            float(weight), unit, measures=[Weight]
        )
        form.instance.items = subscription['items']
        form.instance.metadata = subscription['metadata']
        return super().form_valid(form)


class SubscriptionDoneView(TemplateView):
    template_name = 'storefront/subscription/done.html'


# stripe listen --forward-to localhost:8000/stripe-webhook/
@csrf_exempt
@require_POST
def stripe_webhook(request):
    # You can use webhooks to receive information about asynchronous payment events.
    # For more about our webhook events check out https://stripe.com/docs/webhooks.
    endpoint_secret = settings.STRIPE_WEBHOOK_SECRET
    payload = request.body
    sig_header = request.META['HTTP_STRIPE_SIGNATURE']
    event = None

    try:
        event = stripe.Webhook.construct_event(
            payload, sig_header, endpoint_secret
        )
    except ValueError as e:
        # Invalid payload
        logger.warning('Stripe Webhook: Invalid payload')
        return JsonResponse({'status': 'ERROR: Invalid payload'})
    except stripe.error.SignatureVerificationError as e:
        # Invalid signature
        logger.warning('Stripe Webhook: Invalid signature')
        return JsonResponse({'status': 'ERROR: Invalid signature'})

    if event.type == 'customer.subscription.created':
        try:
            subscription = Subscription.objects.get(
                pk=event.data.object['metadata'].get('subscription_pk')
            )
        except Subscription.DoesNotExist:
            logger.warning('Subscription does not exist')
            raise
        else:
            subscription.stripe_id = event.data.object['id']
            subscription.is_active = True
            subscription.save()

    if event.type == 'invoice.paid':
        # Continue to provision the subscription as payments continue to be made.
        # Store the status in your database and check when a user accesses your service.
        # This approach helps you avoid hitting rate limits.
        try:
            subscription = Subscription.objects.get(
                stripe_id=event.data.object['subscription']
            )
        except Subscription.DoesNotExist:
            logger.warning('Subscription does not exist')
            raise
        else:
            subscription.create_order(event.data.object)

    return JsonResponse({'status': 'success'})
