import itertools
import json
import os
import random
import unicodedata
import uuid
from collections import defaultdict
from typing import Type, Union
from unittest.mock import patch

from django.conf import settings
from django.contrib.sites.models import Site
from django.core.files import File
from django.utils import timezone
from faker import Factory
from faker.providers import BaseProvider
from measurement.measures import Weight
from prices import Money, TaxedMoney

from ...account.models import Address, User
from ...account.utils import store_user_address
from ...checkout import AddressType
from ...core.weight import zero_weight
from ...discount import DiscountValueType, VoucherType
from ...discount.models import Sale, Voucher
from ...discount.utils import fetch_discounts
from ...extensions.manager import get_extensions_manager
from ...giftcard.models import GiftCard
from ...menu.models import Menu
from ...menu.utils import update_menu
from ...order.models import Fulfillment, Order, OrderLine
from ...order.utils import update_order_status
from ...page.models import Page
from ...payment import gateway
from ...payment.utils import create_payment
from ...product.models import (
	AssignedProductAttribute,
	AssignedVariantAttribute,
	Attribute,
	AttributeProduct,
	AttributeValue,
	AttributeVariant,
	Category,
	Collection,
	CollectionProduct,
	Product,
	ProductImage,
	ProductType,
	ProductVariant
)

from ...product.tasks import update_products_minimal_variant_prices_of_discount_task
from ...product.thumbnails import (
	create_category_background_image_thumbnails,
	create_collection_background_image_thumbnails,
	create_product_thumbnails,
)
from ...shipping.models import ShippingMethod, ShippingMethodType, ShippingZone

from ...static.scrapers.datasets.Refined.generate_clean_data import scraper_collections, START_PRODUCT, START_PRODUCTIMAGE
import jsonlines as jl

fake = Factory.create()

def get_images_mapping():
	IMAGES_MAPPING = {}

	index_product = START_PRODUCT
	index_image = START_PRODUCTIMAGE

	file_ext = '.jl'
	raw_dir = os.path.join(
		settings.PROJECT_ROOT, "kaavish", "static", "scrapers", "datasets", "Raw\\"
	)

	for scraper in scraper_collections:
		with jl.open(raw_dir+scraper_collections[scraper]+file_ext) as reader:
			for obj in reader:
				temp_images = []
				for img in obj["images"]:
					temp_images.append(img["path"])
					index_image += 1
				IMAGES_MAPPING[index_product] = temp_images
				index_product += 1
	return IMAGES_MAPPING

IMAGES_MAPPING = get_images_mapping()

def get_weight(weight):
	if not weight:
		return zero_weight()
	value, unit = weight.split()
	return Weight(**{unit: value})


def create_product_types(product_type_data):
	for product_type in product_type_data:
		pk = product_type["pk"]
		defaults = product_type["fields"]
		defaults["weight"] = get_weight(defaults["weight"])
		ProductType.objects.update_or_create(pk=pk, defaults=defaults)


def create_categories(categories_data, placeholder_dir):
	for category in categories_data:
		pk = category["pk"]
		defaults = category["fields"]
		image_name = defaults["background_image"]
		background_image = get_image(placeholder_dir, image_name)
		defaults["background_image"] = background_image
		Category.objects.update_or_create(pk=pk, defaults=defaults)
		create_category_background_image_thumbnails.delay(pk)


def create_collections(data, placeholder_dir):
	for collection in data:
		pk = collection["pk"]
		defaults = collection["fields"]
		image_name = defaults["background_image"]
		background_image = get_image(placeholder_dir, image_name)
		defaults["background_image"] = background_image
		Collection.objects.update_or_create(pk=pk, defaults=defaults)
		create_collection_background_image_thumbnails.delay(pk)


def assign_products_to_collections(associations: list):
	for value in associations:
		pk = value["pk"]
		defaults = value["fields"]
		defaults["collection_id"] = defaults.pop("collection")
		defaults["product_id"] = defaults.pop("product")
		CollectionProduct.objects.update_or_create(pk=pk, defaults=defaults)

def create_attributes(attributes_data):
	for attribute in attributes_data:
		pk = attribute["pk"]
		defaults = attribute["fields"]
		attr, _ = Attribute.objects.update_or_create(pk=pk, defaults=defaults)

def create_product_variants(variants_data):
    for variant in variants_data:
        pk = variant["pk"]
        defaults = variant["fields"]
        defaults["weight"] = get_weight(defaults["weight"])
        product_id = defaults.pop("product")
        # We have not created products without images
        if product_id not in IMAGES_MAPPING:
            continue
        defaults["product_id"] = product_id
        set_field_as_money(defaults, "price_override")
        set_field_as_money(defaults, "cost_price")
        ProductVariant.objects.update_or_create(pk=pk, defaults=defaults)

def create_attributes_values(values_data):
	for value in values_data:
		pk = value["pk"]
		defaults = value["fields"]
		defaults["attribute_id"] = defaults.pop("attribute")
		AttributeValue.objects.update_or_create(pk=pk, defaults=defaults)


def create_products(products_data, placeholder_dir, create_images):
	for product in products_data:
		pk = product["pk"]

		defaults = product["fields"]
		set_field_as_money(defaults, "price")
		defaults["weight"] = get_weight(defaults["weight"])
		defaults["category_id"] = defaults.pop("category")
		defaults["product_type_id"] = defaults.pop("product_type")

		gen_product, _ = Product.objects.update_or_create(pk=pk, defaults=defaults)

		if create_images:
			images = IMAGES_MAPPING.get(pk, [])
			for image_name in images:
				create_product_image(gen_product, placeholder_dir, image_name)

def assign_attributes_to_product_types(
	association_model: Union[Type[AttributeProduct]],
	attributes: list,
):
	for value in attributes:
		pk = value["pk"]
		defaults = value["fields"]
		defaults["attribute_id"] = defaults.pop("attribute")
		defaults["product_type_id"] = defaults.pop("product_type")
		association_model.objects.update_or_create(pk=pk, defaults=defaults)


def assign_attributes_to_products(product_attributes):
	for value in product_attributes:
		pk = value["pk"]
		defaults = value["fields"]
		defaults["product_id"] = defaults.pop("product")
		defaults["assignment_id"] = defaults.pop("assignment")
		assigned_values = defaults.pop("values")
		assoc, created = AssignedProductAttribute.objects.update_or_create(
			pk=pk, defaults=defaults
		)
		if created:
			assoc.values.set(AttributeValue.objects.filter(pk__in=assigned_values))


def set_field_as_money(defaults, field):
	amount_field = f"{field}_amount"
	if amount_field in defaults and defaults[amount_field] is not None:
		defaults[field] = Money(defaults[amount_field], settings.DEFAULT_CURRENCY)

def assign_attributes_to_product_types(
    association_model: Union[Type[AttributeProduct], Type[AttributeVariant]],
    attributes: list,
):
    for value in attributes:
        pk = value["pk"]
        defaults = value["fields"]
        defaults["attribute_id"] = defaults.pop("attribute")
        defaults["product_type_id"] = defaults.pop("product_type")
        association_model.objects.update_or_create(pk=pk, defaults=defaults)

def assign_attributes_to_variants(variant_attributes):
    for value in variant_attributes:
        pk = value["pk"]
        defaults = value["fields"]
        defaults["variant_id"] = defaults.pop("variant")
        defaults["assignment_id"] = defaults.pop("assignment")
        assigned_values = defaults.pop("values")
        assoc, created = AssignedVariantAttribute.objects.update_or_create(
            pk=pk, defaults=defaults
        )
        if created:
            assoc.values.set(AttributeValue.objects.filter(pk__in=assigned_values))

def create_products_by_schema(placeholder_dir, create_images):
	path = os.path.join(
		settings.PROJECT_ROOT, "kaavish", "static", "scrapers", "datasets", "Refined", "clean_data.json"
	)
	with open(path) as f:
		db_items = json.load(f)
	types = defaultdict(list)
	# Sort db objects by its model
	for item in db_items:
		model = item.pop("model")
		types[model].append(item)

	create_product_types(product_type_data=types["product.producttype"])
	create_categories(
		categories_data=types["product.category"], placeholder_dir=placeholder_dir
	)
	create_attributes(attributes_data=types["product.attribute"])
	create_attributes_values(values_data=types["product.attributevalue"])
	create_products(
		products_data=types["product.product"],
		placeholder_dir=placeholder_dir,
		create_images=create_images,
	)
	create_product_variants(variants_data=types["product.productvariant"])
	assign_attributes_to_product_types(
		AttributeProduct, attributes=types["product.attributeproduct"]
	)
	assign_attributes_to_product_types(
		AttributeVariant, attributes=types["product.attributevariant"]
	)
	assign_attributes_to_products(
		product_attributes=types["product.assignedproductattribute"]
	)
	assign_attributes_to_variants(
		variant_attributes=types["product.assignedvariantattribute"]
	)

	create_collections(
		data=types["product.collection"], placeholder_dir=placeholder_dir
	)

	assign_products_to_collections(associations=types["product.collectionproduct"])


class kaavishProvider(BaseProvider):
	def money(self):
		return Money(fake.pydecimal(2, 2, positive=True), settings.DEFAULT_CURRENCY)

	def weight(self):
		return Weight(kg=fake.pydecimal(1, 2, positive=True))


fake.add_provider(kaavishProvider)


def get_email(first_name, last_name):
	_first = unicodedata.normalize("NFD", first_name).encode("ascii", "ignore")
	_last = unicodedata.normalize("NFD", last_name).encode("ascii", "ignore")
	return "%s.%s@example.com" % (
		_first.lower().decode("utf-8"),
		_last.lower().decode("utf-8"),
	)


def create_product_image(product, placeholder_dir, image_name):
	image = get_image(placeholder_dir, image_name)
	# We don't want to create duplicated product images
	if product.images.count() >= len(IMAGES_MAPPING.get(product.pk, [])):
		return None
	product_image = ProductImage(product=product, image=image)
	product_image.save()
	create_product_thumbnails.delay(product_image.pk)
	return product_image

def create_address():
	address = Address.objects.create(
		first_name=fake.first_name(),
		last_name=fake.last_name(),
		street_address_1=fake.street_address(),
		city=fake.city(),
		postal_code=fake.postcode(),
		country=settings.DEFAULT_COUNTRY,
	)
	return address

def create_fake_user():
	address = create_address()
	email = get_email(address.first_name, address.last_name)

	# Skip the email if it already exists
	if User.objects.filter(email=email).exists():
		return

	user = User.objects.create_user(
		first_name=address.first_name,
		last_name=address.last_name,
		email=email,
		password="password",
	)

	user.addresses.add(address)
	user.default_billing_address = address
	user.default_shipping_address = address
	user.is_active = True
	user.save()
	return user

# We don't want to spam the console with payment confirmations sent to
# fake customers.
@patch("kaavish.order.emails.send_payment_confirmation.delay")
def create_fake_payment(mock_email_confirmation, order):
	payment = create_payment(
		gateway="Dummy",
		customer_ip_address=fake.ipv4(),
		email=order.user_email,
		order=order,
		payment_token=str(uuid.uuid4()),
		total=order.total.gross.amount,
		currency=order.total.gross.currency,
		billing_address=order.billing_address,
	)

	# Create authorization transaction
	gateway.authorize(payment, payment.token)
	# 20% chance to void the transaction at this stage
	if random.choice([0, 0, 0, 0, 1]):
		gateway.void(payment)
		return payment
	# 25% to end the payment at the authorization stage
	if not random.choice([1, 1, 1, 0]):
		return payment
	# Create capture transaction
	gateway.capture(payment)
	# 25% to refund the payment
	if random.choice([0, 0, 0, 1]):
		gateway.refund(payment)
	return payment


# Todo: Fix - Not working since variant quantity is never calculated/populated
def create_order_lines(order, discounts, how_many=10):
	variants = (
		Product.objects.filter()
		.order_by("?")
		.prefetch_related("product__product_type")[:how_many]
	)
	products_iter = itertools.cycle(variants)
	lines = []
	for dummy in range(how_many):
		product = next(products_iter)
		product = product.product
		quantity = random.randrange(1, 5)
		product.quantity += quantity
		product.quantity_allocated += quantity
		unit_price = product.get_price(discounts)
		unit_price = TaxedMoney(net=unit_price, gross=unit_price)
		lines.append(
			OrderLine(
				order=order,
				product_name=str(product),
				product_sku=product.sku,
				is_shipping_required=product.is_shipping_required(),
				quantity=quantity,
				product=product,
				unit_price=unit_price,
				tax_rate=0,
			)
		)
	Product.objects.bulk_update(variants, ["quantity", "quantity_allocated"])
	lines = OrderLine.objects.bulk_create(lines)
	manager = get_extensions_manager()
	for line in lines:
		unit_price = manager.calculate_order_line_unit(line)
		line.unit_price = unit_price
		line.tax_rate = unit_price.tax / unit_price.net
	OrderLine.objects.bulk_update(
		lines,
		["unit_price_net_amount", "unit_price_gross_amount", "currency", "tax_rate"],
	)
	return lines


def create_fulfillments(order):
	for line in order:
		if random.choice([False, True]):
			fulfillment, _ = Fulfillment.objects.get_or_create(order=order)
			quantity = random.randrange(0, line.quantity) + 1
			fulfillment.lines.create(order_line=line, quantity=quantity)
			line.quantity_fulfilled = quantity
			line.save(update_fields=["quantity_fulfilled"])

	update_order_status(order)


def create_fake_order(discounts, max_order_lines=5):
	user = random.choice(
		[None, User.objects.filter(is_superuser=False).order_by("?").first()]
	)
	if user:
		address = user.default_shipping_address
		order_data = {
			"user": user,
			"billing_address": user.default_billing_address,
			"shipping_address": address,
		}
	else:
		address = create_address()
		order_data = {
			"billing_address": address,
			"shipping_address": address,
			"user_email": get_email(address.first_name, address.last_name),
		}

	manager = get_extensions_manager()
	shipping_method = ShippingMethod.objects.order_by("?").first()
	shipping_price = shipping_method.price
	shipping_price = manager.apply_taxes_to_shipping(shipping_price, address)
	order_data.update(
		{"shipping_method_name": shipping_method.name, "shipping_price": shipping_price}
	)

	order = Order.objects.create(**order_data)

	lines = create_order_lines(order, discounts, random.randrange(1, max_order_lines))
	order.total = sum([line.get_total() for line in lines], shipping_price)
	weight = Weight(kg=0)
	for line in order:
		weight += line.variant.get_weight()
	order.weight = weight
	order.save()

	create_fake_payment(order=order)
	create_fulfillments(order)
	return order


def create_fake_sale():
	sale = Sale.objects.create(
		name="Happy %s day!" % fake.word(),
		type=DiscountValueType.PERCENTAGE,
		value=random.choice([10, 20, 30, 40, 50]),
	)
	for product in Product.objects.all().order_by("?")[:4]:
		sale.products.add(product)
	return sale


def create_users(how_many=10):
	for dummy in range(how_many):
		user = create_fake_user()
		yield "User: %s" % (user.email,)


def create_orders(how_many=10):
	discounts = fetch_discounts(timezone.now())
	for _ in range(how_many):
		order = create_fake_order(discounts)
		yield "Order: %s" % (order,)


def create_product_sales(how_many=5):
	for dummy in range(how_many):
		sale = create_fake_sale()
		update_products_minimal_variant_prices_of_discount_task.delay(sale.pk)
		yield "Sale: %s" % (sale,)


def create_shipping_zone(shipping_methods_names, countries, shipping_zone_name):
	shipping_zone = ShippingZone.objects.get_or_create(
		name=shipping_zone_name, defaults={"countries": countries}
	)[0]
	ShippingMethod.objects.bulk_create(
		[
			ShippingMethod(
				name=name,
				price=fake.money(),
				shipping_zone=shipping_zone,
				type=(
					ShippingMethodType.PRICE_BASED
					if random.randint(0, 1)
					else ShippingMethodType.WEIGHT_BASED
				),
				minimum_order_price=Money(0, settings.DEFAULT_CURRENCY),
				maximum_order_price_amount=None,
				minimum_order_weight=0,
				maximum_order_weight=None,
			)
			for name in shipping_methods_names
		]
	)
	return "Shipping Zone: %s" % shipping_zone


def create_shipping_zones():
	asian_countries = [ "PK" ]
	yield create_shipping_zone(
		shipping_zone_name="Asia",
		countries=asian_countries,
		shipping_methods_names=["TCS", "Leopard Courier", "Pakistan Post"],
	)


def create_vouchers():
	voucher, created = Voucher.objects.get_or_create(
		code="FREESHIPPING",
		defaults={
			"type": VoucherType.SHIPPING,
			"name": "Free shipping",
			"discount_value_type": DiscountValueType.PERCENTAGE,
			"discount_value": 100,
		},
	)
	if created:
		yield "Voucher #%d" % voucher.id
	else:
		yield "Shipping voucher already exists"

	voucher, created = Voucher.objects.get_or_create(
		code="DISCOUNT",
		defaults={
			"type": VoucherType.ENTIRE_ORDER,
			"name": "Big order discount",
			"discount_value_type": DiscountValueType.FIXED,
			"discount_value": 25,
			"min_spent": Money(200, settings.DEFAULT_CURRENCY),
		},
	)
	if created:
		yield "Voucher #%d" % voucher.id
	else:
		yield "Value voucher already exists"


def create_gift_card():
	user = random.choice(
		[User.objects.filter(is_superuser=False).order_by("?").first()]
	)
	gift_card, created = GiftCard.objects.get_or_create(
		code="Gift_card_10",
		defaults={
			"user": user,
			"initial_balance": Money(10, settings.DEFAULT_CURRENCY),
			"current_balance": Money(10, settings.DEFAULT_CURRENCY),
		},
	)
	if created:
		yield "Gift card #%d" % gift_card.id
	else:
		yield "Gift card already exists"


def set_homepage_collection():
	homepage_collection = Collection.objects.order_by("?").first()
	site = Site.objects.get_current()
	site_settings = site.settings
	site_settings.homepage_collection = homepage_collection
	site_settings.save()
	yield "Homepage collection assigned"


def add_address_to_admin(email):
	address = create_address()
	user = User.objects.get(email=email)
	store_user_address(user, address, AddressType.BILLING)
	store_user_address(user, address, AddressType.SHIPPING)


def create_page():
	# Creates the About page
	content = """
	<h2>Kaavish</h2>
	<p>Personal Outfit Recommendation System (PORS) - FYP 2020</p>
	<p><a href="https://github.com/osama-usuf/Kaavish-PORS">About</a></p>
	"""
	content_json = {
		"blocks": [
			{
				"key": "",
				"data": {},
				"text": "FYP 2020",
				"type": "header-two",
				"depth": 0,
				"entityRanges": [],
				"inlineStyleRanges": [],
			},
			{
				"key": "",
				"data": {},
				"text": "Kaavish - Personal Outfit Recommendation System - Habib University",
				"type": "unstyled",
				"depth": 0,
				"entityRanges": [],
				"inlineStyleRanges": [],
			},
			{
				"key": "",
				"data": {},
				"text": "Muhammad Ali",
				"type": "unstyled",
				"depth": 0,
				"entityRanges": [],
				"inlineStyleRanges": [],
			},
			{
				"key": "",
				"data": {},
				"text": "Osama Yousuf",
				"type": "unstyled",
				"depth": 0,
				"entityRanges": [],
				"inlineStyleRanges": [],
			},
			{
				"key": "",
				"data": {},
				"text": "Syeda Areeba Kazmi",
				"type": "unstyled",
				"depth": 0,
				"entityRanges": [],
				"inlineStyleRanges": [],
			},
			{
				"key": "",
				"data": {},
				"text": "Tasneem Adnan",
				"type": "unstyled",
				"depth": 0,
				"entityRanges": [{"key": 0, "length": 17, "offset": 0}],
				"inlineStyleRanges": [],
			},
		],
		"entityMap": {
			"0": {
				"data": {"url": "https://github.com/osama-usuf/Kaavish-PORS"},
				"type": "LINK",
				"mutability": "MUTABLE",
			}
		},
	}
	page_data = {
		"content": content,
		"content_json": content_json,
		"title": "About",
		"is_published": True,
	}
	page, dummy = Page.objects.get_or_create(slug="about", defaults=page_data)
	yield "Page %s created" % page.slug


def generate_menu_items(menu: Menu, category: Category, parent_menu_item):
	menu_item, created = menu.items.get_or_create(
		name=category.name, category=category, parent=parent_menu_item
	)

	if created:
		yield "Created menu item for category %s" % category

	for child in category.get_children():
		for msg in generate_menu_items(menu, child, menu_item):
			yield "\t%s" % msg


def generate_menu_tree(menu):
	categories = Category.tree.get_queryset().filter(products__isnull=False)
	for category in categories:
		if not category.parent_id:
			for msg in generate_menu_items(menu, category, None):
				yield msg


def create_menus():
	# Create navbar menu with category links
	top_menu, _ = Menu.objects.get_or_create(
		name=settings.DEFAULT_MENUS["top_menu_name"]
	)
	top_menu.items.all().delete()
	yield "Created navbar menu"
	for msg in generate_menu_tree(top_menu):
		yield msg

	# Create footer menu with collections and pages
	bottom_menu, _ = Menu.objects.get_or_create(
		name=settings.DEFAULT_MENUS["bottom_menu_name"]
	)
	bottom_menu.items.all().delete()
	collection = Collection.objects.filter(products__isnull=False).order_by("?")[0]
	item, _ = bottom_menu.items.get_or_create(name="Collections", collection=collection)

	for collection in Collection.objects.filter(
		products__isnull=False, background_image__isnull=False
	):
		bottom_menu.items.get_or_create(
			name=collection.name, collection=collection, parent=item
		)

	# Bottom menu about page is not being populated, FIX.
	page = Page.objects.order_by("?")[0]
	bottom_menu.items.get_or_create(name=page.title, page=page)
	yield "Created footer menu"
	update_menu(top_menu)
	update_menu(bottom_menu)
	site = Site.objects.get_current()
	site_settings = site.settings
	site_settings.top_menu = top_menu
	site_settings.bottom_menu = bottom_menu
	site_settings.save()


def get_image(image_dir, image_name):
	img_path = os.path.join(image_dir, image_name)
	return File(open(img_path, "rb"), name=image_name)