using DiplomovaPrace.Contracts.Customer;
using DiplomovaPrace.Contracts.Customer.Orders;
using DiplomovaPrace.Contracts.Customer.Orders.InputModels;
using DiplomovaPrace.Contracts.Customer.Orders.ViewModels;
using DiplomovaPrace.DataLayer.Queries.UserOrders;
using DiplomovaPrace.DataLayer.Repositories.Orders;
using DiplomovaPrace.DataLayer.Repositories.Products;
using DiplomovaPrace.DataLayer.Repositories.Security;
using DiplomovaPrace.Model.Orders;
using DiplomovaPrace.Model.Products;
using DiplomovaPrace.Model.Security;
using DiplomovaPrace.Services.Mailer;
using DiplomovaPrace.Services.Orders;
using Havit.Data.Patterns.DataLoaders;
using Havit.Data.Patterns.UnitOfWorks;
using Havit.Diagnostics.Contracts;
using Havit.Extensions.DependencyInjection.Abstractions;
using Havit.Security;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace DiplomovaPrace.Facades.Customer.Orders
{
	[Service]
	public class OrderFacade : IOrderFacade
	{
		private readonly IAddressRepository addressRepository;
		private readonly IAdminOrderPriceCalculator adminOrderPriceCalculator;
		private readonly IDataLoader dataLoader;
		private readonly IDeliveryTypeRepository deliveryTypeRepository;
		private readonly ILoginAccountRepository loginAccountRepository;
		private readonly IMailerService mailerService;
		private readonly IOrderListQuery orderListQuery;
		private readonly IOrderRepository orderRepository;
		private readonly IPaymentTypeRepository paymentTypeRepository;
		private readonly IProductRepository productRepository;
		private readonly IRoleRepository roleRepository;
		private readonly IUnitOfWork unitOfWork;

		public OrderFacade(
		IAddressRepository addressRepository,
		IAdminOrderPriceCalculator adminOrderPriceCalculator,
		IDataLoader dataLoader,
		IDeliveryTypeRepository deliveryTypeRepository,
		ILoginAccountRepository loginAccountRepository,
		IMailerService mailerService,
		IOrderListQuery orderListQuery,
		IOrderRepository orderRepository,
		IPaymentTypeRepository paymentTypeRepository,
		IProductRepository productRepository,
		IRoleRepository roleRepository,
		IUnitOfWork unitOfWork)
		{
			this.addressRepository = addressRepository;
			this.adminOrderPriceCalculator = adminOrderPriceCalculator;
			this.dataLoader = dataLoader;
			this.deliveryTypeRepository = deliveryTypeRepository;
			this.loginAccountRepository = loginAccountRepository;
			this.mailerService = mailerService;
			this.orderListQuery = orderListQuery;
			this.orderRepository = orderRepository;
			this.paymentTypeRepository = paymentTypeRepository;
			this.productRepository = productRepository;
			this.roleRepository = roleRepository;
			this.unitOfWork = unitOfWork;
		}

		/// <summary>
		/// Adds new order item to the order or raise the amount of already added order item in the order
		/// </summary>
		/// <param name="userID"></param>
		/// <param name="orderItemIM"></param>
		/// <returns>New amount of order items in the order</returns>
		public async Task<ShoppingCartItemsAmountVM> AddOrderItemAsync(int userID, OrderItemIM orderItemIM)
		{
			Order order = await orderRepository.GetOrderInCartByCustomerIDAsync(userID);
			// user has order in the state: shopping cart
			if (order is not null)
			{
				await dataLoader.LoadAsync(order, i => i.OrderItems);
				await dataLoader.LoadAllAsync(order.OrderItems, i => i.Product);

				OrderItem orderItem = order.OrderItems.Find(i => i.ProductID == orderItemIM.ProductID);
				// order item is already in the order
				if (orderItem is not null)
				{
					Product product = await productRepository.GetObjectAsync(orderItemIM.ProductID);
					if (product is not null)
					{
						//product.AvailableQuantity -= orderItemIM.Amount; // decrease the amount in order confirm method not here!
						if ((product.AvailableQuantity - (orderItem.Amount + orderItemIM.Amount)) < 0) return null; // inform user that the current product is not available

						orderItem.Amount += orderItemIM.Amount;
						orderItem.Price = (decimal)((product.ReducedPrice != null && product.ReducedPrice > 0 && product.ReducedPrice < product.Price)
								? product.ReducedPrice
								: product.Price);

						//unitOfWork.AddForUpdate(product);
						unitOfWork.AddForUpdate(orderItem);
					}
					else
					{
						return null; // could not find the product => do nothing and inform user that something went wrong
					}

					await unitOfWork.CommitAsync();
					return new ShoppingCartItemsAmountVM() { Amount = order.OrderItems.Count };
				}
				else // order item is not in the order
				{
					Product product = await productRepository.GetObjectAsync(orderItemIM.ProductID);
					if (product is not null)
					{
						//product.AvailableQuantity -= orderItemIM.Amount; // decrease the amount in order confirm method not here!
						if ((product.AvailableQuantity - orderItemIM.Amount) < 0) return null; // inform user that the current product is not available

						orderItem = new()
						{
							OrderID = order.ID,
							Amount = orderItemIM.Amount,
							Price = (decimal)((product.ReducedPrice != null && product.ReducedPrice > 0 && product.ReducedPrice < product.Price)
								? product.ReducedPrice
								: product.Price),
							ProductID = product.ID
						};

						//unitOfWork.AddForUpdate(product);
						unitOfWork.AddForInsert(orderItem);
					}
					else
					{
						return null; // could not find the product => do nothing and inform user that something went wrong
					}

					await unitOfWork.CommitAsync();
					return new ShoppingCartItemsAmountVM() { Amount = order.OrderItems.Count };
				}
			}
			else // user does not have an order in the state: shopping cart
			{
				Order newOrder = new()
				{
					LoginAccountID = userID,
					OrderStateID = Math.Abs((int)OrderState.Entry.ShoppingCart), // not the best solution
					TotalPrice = 0
				};

				unitOfWork.AddForInsert(newOrder);
				await unitOfWork.CommitAsync();

				Product product = await productRepository.GetObjectAsync(orderItemIM.ProductID);
				if (product is not null)
				{
					//product.AvailableQuantity -= orderItemIM.Amount; // decrease the amount in order confirm method not here!
					if ((product.AvailableQuantity - orderItemIM.Amount) < 0) return null; // inform user that the current product is not available

					OrderItem newOrderItem = new()
					{
						OrderID = newOrder.ID,
						Amount = orderItemIM.Amount,
						Price = (decimal)((product.ReducedPrice != null && product.ReducedPrice > 0 && product.ReducedPrice < product.Price)
								? product.ReducedPrice
								: product.Price),
						ProductID = product.ID
					};

					//unitOfWork.AddForUpdate(product);
					unitOfWork.AddForInsert(newOrderItem);
				}
				else
				{
					return null; // could not find the product => do nothing and inform user that something went wrong
				}

				await unitOfWork.CommitAsync();
				return new ShoppingCartItemsAmountVM() { Amount = 1 };
			}
		}

		public async Task<NewOrderVM> CreateOrderAsync(OrderIM inputModel)
		{
			Order newOrder = new();
			DeliveryType deliveryType = await deliveryTypeRepository.GetObjectAsync(inputModel.DeliveryTypeID);
			PaymentType paymentType = await paymentTypeRepository.GetObjectAsync(inputModel.PaymentTypeID);

			// Lepe poskladat adresy, aby se omezila redundance kodu
			// Rovnou priradit stejne property a ostatni pak dynamicky doplnit

			// Neregistrovany uzivatel
			if (inputModel.UserID is null)
			{
				LoginAccount account = await loginAccountRepository.GetByEmailAsync(inputModel.InvoicingAddress.Email);
				// Pokud je emailová adresa registrována tak:
					// Vyžádat po uživateli přihlášení?
					// Přiřadit objednávku na registrovaný email?
					// Nepřiřazovat objednávku (uživatel neuvidí objednávku ve výpisu objednávek)
				if (account is not null && inputModel.Password is not null)
				{
					return new NewOrderVM() { ErrorMessage = "Zadaná emailová adresa je již registrována!" }; // Cannot register new acc if the email is already registered
				}

				// Vytvorim fakturacni adresu
				Address newInvoicingAddress = new()
				{
					FirstName = inputModel.InvoicingAddress.FirstName,
					LastName = inputModel.InvoicingAddress.LastName,
					PhoneNumber = inputModel.InvoicingAddress.PhoneNumber,
					City = inputModel.InvoicingAddress.City,
					Street = inputModel.InvoicingAddress.Street,
					ZipCode = inputModel.InvoicingAddress.ZipCode,
					State = "Česká republika",
					AddressTypeID = (int)AddressType.Entry.InvoicingAddress + 1 // SQL starts from 1
				};
				unitOfWork.AddForInsert(newInvoicingAddress);

				// Vytvorim dorucovaci adresu
				Address newDeliveryAddress = null;
				if (inputModel.DeliveryAddress is not null)
				{
					newDeliveryAddress = new()
					{
						FirstName = inputModel.DeliveryAddress.FirstName,
						LastName = inputModel.DeliveryAddress.LastName,
						PhoneNumber = inputModel.DeliveryAddress.PhoneNumber,
						City = inputModel.DeliveryAddress.City,
						Street = inputModel.DeliveryAddress.Street,
						ZipCode = inputModel.DeliveryAddress.ZipCode,
						State = "Česká republika",
						AddressTypeID = (int)AddressType.Entry.DeliveryAddress + 1 // SQL starts from 1
					};
					unitOfWork.AddForInsert(newDeliveryAddress);
				}

				// Vytvorit ucet
				if (inputModel.Password is not null)
				{
					string salt = PasswordHashService.CreateSalt();

					LoginAccount loginAccount = new()
					{
						FirstName = inputModel.InvoicingAddress.FirstName,
						LastName = inputModel.InvoicingAddress.LastName,
						Email = inputModel.InvoicingAddress.Email,
						RoleID = (int?)Role.Entry.User + 1,
						PasswordHash = PasswordHashService.ComputeSHA512HashString(inputModel.Password.Password, salt),
						PasswordSalt = salt
					};

					unitOfWork.AddForInsert(loginAccount);
					await unitOfWork.CommitAsync();

					newOrder.LoginAccountID = loginAccount.ID;

					newInvoicingAddress.LoginAccountID = loginAccount.ID;
					unitOfWork.AddForUpdate(newInvoicingAddress);

					if (newDeliveryAddress is not null)
					{
						newDeliveryAddress.LoginAccountID = loginAccount.ID;
						unitOfWork.AddForUpdate(newDeliveryAddress);
					}
				}
				else // Nevytvaret ucet (objekt Password je prazdny)
				{
					newOrder.CustomerEmail = inputModel.InvoicingAddress.Email;
				}

				// Pohlidat si jestli mam dobre commit
				await unitOfWork.CommitAsync();
				newOrder.InvoicingAddressID = newInvoicingAddress.ID;
				newOrder.DeliveryAddressID = newDeliveryAddress?.ID;
			}
			else // Registrovany a prihlaseny uzivatel
			{
				LoginAccount user = await loginAccountRepository.GetObjectAsync((int)inputModel.UserID);
				LoginAccount account = await loginAccountRepository.GetByEmailAsync(inputModel.InvoicingAddress.Email);
				if (account is not null && account.ID != user.ID)
				{
					return new NewOrderVM() { ErrorMessage = "Zadaná emailová adresa je již registrována na jiný účet!" }; // User entered email address of differenet login account
				}

				newOrder = await orderRepository.GetOrderInCartByCustomerIDAsync(user.ID);
				await dataLoader.LoadAsync(newOrder, i => i.OrderItems);
				await dataLoader.LoadAllAsync(newOrder.OrderItems, i => i.Product);

				Address newInvoicingAddress = new()
				{
					FirstName = inputModel.InvoicingAddress.FirstName,
					LastName = inputModel.InvoicingAddress.LastName,
					PhoneNumber = inputModel.InvoicingAddress.PhoneNumber,
					City = inputModel.InvoicingAddress.City,
					Street = inputModel.InvoicingAddress.Street,
					ZipCode = inputModel.InvoicingAddress.ZipCode,
					State = "Česká republika",
					LoginAccountID = user.ID,
					AddressTypeID = (int)AddressType.Entry.InvoicingAddress + 1 // SQL starts from 1
				};

				if (inputModel.DeliveryAddress is not null) // Pokud neni dorucovaci adresa v input modelu null
				{
					Address newDeliveryAddress = new()
					{
						FirstName = inputModel.DeliveryAddress.FirstName,
						LastName = inputModel.DeliveryAddress.LastName,
						PhoneNumber = inputModel.DeliveryAddress.PhoneNumber,
						City = inputModel.DeliveryAddress.City,
						Street = inputModel.DeliveryAddress.Street,
						ZipCode = inputModel.DeliveryAddress.ZipCode,
						State = "Česká republika",
						LoginAccountID = user.ID,
						AddressTypeID = (int)AddressType.Entry.DeliveryAddress + 1 // SQL starts from 1
					};

					if (inputModel.DeliveryAddress.ID != null) // pokud obsahuje ID, tak porovnam s existujici dorucovaci adresou
					{
						Address deliveryAddress = await addressRepository.GetObjectAsync((int)inputModel.DeliveryAddress.ID);

						// pokud jsou prichozi a existujici adresa stejne, tak jen priradim ID existujici dorucovaci adresy k objednavce
						if (AreAddressesEquals(deliveryAddress, newDeliveryAddress))
						{
							newOrder.DeliveryAddressID = deliveryAddress.ID;
						}
						else // jinak musim zalozit novou
						{
							unitOfWork.AddForInsert(newDeliveryAddress);
							await unitOfWork.CommitAsync();
							newOrder.DeliveryAddressID = newDeliveryAddress.ID;
						}
					}
					else // vytvorim novou dorucovaci adresu
					{
						unitOfWork.AddForInsert(newDeliveryAddress);
						await unitOfWork.CommitAsync();
						newOrder.DeliveryAddressID = newDeliveryAddress.ID;
					}
				}

				// Invoicing address zpracovani
				if (inputModel.InvoicingAddress.ID != null)
				{
					Address invoicingAddress = await addressRepository.GetObjectAsync((int)inputModel.InvoicingAddress.ID);

					// Porovnat a pokud se nerovnaji, tak zalozit novou
					if (AreAddressesEquals(invoicingAddress, newInvoicingAddress))
					{
						newOrder.InvoicingAddressID = invoicingAddress.ID;
					}
					else
					{
						unitOfWork.AddForInsert(newInvoicingAddress);
						await unitOfWork.CommitAsync();
						newOrder.InvoicingAddressID = newInvoicingAddress.ID;
					}
				}
				else // Prichozi adresa nema ID nebo se nerovna s adresou v databazi => vytvorit novou adresu
				{
					unitOfWork.AddForInsert(newInvoicingAddress);
					await unitOfWork.CommitAsync();
					newOrder.InvoicingAddressID = newInvoicingAddress.ID;
				}

				//await unitOfWork.CommitAsync();
			}

			newOrder.DeliveryTypeID = deliveryType?.ID;
			newOrder.PaymentTypeID = paymentType?.ID;
			newOrder.OrderStateID = Math.Abs((int)OrderState.Entry.Accepted);
			newOrder.PaymentStateID = Math.Abs((int)PaymentState.Entry.NotPaid);
			newOrder.OrderedDate = DateTime.Now;

			if (newOrder.ID != default)
			{
				unitOfWork.AddForUpdate(newOrder);  // order already in DB
			}
			else
			{
				unitOfWork.AddForInsert(newOrder);  // new order created by not registered customer
			}

			await unitOfWork.CommitAsync();

			// itemy objednavky
			List<OrderItem> orderItems = new();

			// Nema smysl kontrolovat Count > 0
			if (newOrder.OrderItems is not null && newOrder.OrderItems.Count > 0)
			{
				foreach (var orderItem in newOrder.OrderItems)
				{
					Product product = await productRepository.GetObjectAsync(orderItem.ProductID);
					if (product is not null)
					{
						// check if the price of the product hasn't changed
						if (product.ReducedPrice != null && product.ReducedPrice > 0 && product.ReducedPrice < product.Price && product.ReducedPrice != orderItem.Price) // check reduced price
						{
							orderItem.Price = (decimal)product.ReducedPrice;
							unitOfWork.AddForUpdate(orderItem);
						}
						else if ((product.ReducedPrice == null || product.ReducedPrice <= 0) && product.Price != orderItem.Price) // check normal price
						{
							orderItem.Price = product.Price;
							unitOfWork.AddForUpdate(orderItem);
						}

						product.AvailableQuantity -= orderItem.Amount;
						unitOfWork.AddForUpdate(product);

						orderItems.Add(orderItem);
					}
				}
			}
			else
			{
				foreach (var item in inputModel.OrderItems)
				{
					Product product = await productRepository.GetObjectAsync(item.ProductID);
					if (product is not null)
					{
						// Mohl bych řešit v cyklu nahoře, kde kontroluji pouze změnu ceny produktu
						product.AvailableQuantity -= item.Amount; // check negative value

						OrderItem orderItem = new()
						{
							Amount = item.Amount,
							Price = (decimal)((product.ReducedPrice != null && product.ReducedPrice > 0 && product.ReducedPrice < product.Price)
								? product.ReducedPrice
								: product.Price), // check reduced price
							ProductID = item.ProductID,
							OrderID = newOrder.ID
						};

						orderItems.Add(orderItem);
						unitOfWork.AddForUpdate(product);
						unitOfWork.AddForInsert(orderItem);
					}
				}
			}

			newOrder.TotalPrice = adminOrderPriceCalculator.CalculateTotalOrderPrice(orderItems, deliveryType, paymentType);

			unitOfWork.AddForUpdate(newOrder);
			await unitOfWork.CommitAsync();

			// mailer
			string fullName = $"{inputModel.InvoicingAddress.FirstName} {inputModel.InvoicingAddress.LastName}";
			await mailerService.SendOrderConfirmationEmailAsync(inputModel.InvoicingAddress.Email, fullName, newOrder.ID, newOrder.TotalPrice);

			if (newOrder.PaymentTypeID == (int)PaymentTypeEnum.BankTransfer)
			{
				await mailerService.SendOrderPaymentInformationEmailAsync(inputModel.InvoicingAddress.Email, fullName, newOrder.ID, newOrder.TotalPrice);
			}

			return new NewOrderVM() { OrderNumber = newOrder.ID, TotalPrice = newOrder.TotalPrice };
		}

		/// <summary>
		/// Deletes order item from the order
		/// </summary>
		/// <param name="userID"></param>
		/// <param name="orderItemIM"></param>
		/// <returns>New amount of order items in the order</returns>
		public async Task<ShoppingCartItemsAmountVM> DeleteOrderItemAsync(int userID, OrderItemIM orderItemIM)
		{
			Order order = await orderRepository.GetOrderInCartByCustomerIDAsync(userID);
			if (order is not null)
			{
				await dataLoader.LoadAsync(order, i => i.OrderItems);
				await dataLoader.LoadAllAsync(order.OrderItems, i => i.Product);

				OrderItem orderItem = order.OrderItems.Find(i => i.ProductID == orderItemIM.ProductID);
				Product product = await productRepository.GetObjectAsync(orderItemIM.ProductID);
				if (orderItem is not null && product is not null)
				{
					//product.AvailableQuantity += orderItem.Amount;

					unitOfWork.AddForDelete(orderItem);
					//unitOfWork.AddForUpdate(product);
					await unitOfWork.CommitAsync();

					return new ShoppingCartItemsAmountVM() { Amount = order.OrderItems.Count };
				}

				return null; // could not find the order item or product => do nothing and inform user that something went wrong
			}

			return null; // could not find the order => do nothing and inform user that something went wrong
		}

		public async Task<List<DeliveryTypeVM>> GetDeliveryTypesAsync()
		{
			List<DeliveryType> deliveryTypes = await deliveryTypeRepository.GetAllAsync();

			if (deliveryTypes.Count > 0)
			{
				return deliveryTypes.Select(i =>
				new DeliveryTypeVM
				{
					ID = i.ID,
					Name = i.Name,
					Fee = i.Fee
				}).ToList();
			}

			return null;
		}

		public async Task<PagedQueryResult<OrderVM>> GetPagedOrdersAsync(PagedQueryRequest<OrderListQueryFilter> orderListRequest)
		{
			Contract.Requires<ArgumentNullException>(orderListRequest is not null, nameof(orderListRequest));

			orderListQuery.Filter = orderListRequest.Filter;
			orderListQuery.Sorting = orderListRequest.Sorting;

			return await orderListQuery.GetPageAsync(orderListRequest.PageIndex, orderListRequest.PageSize).ConfigureAwait(false);
		}

		public async Task<List<PaymentTypeVM>> GetPaymentTypesAsync()
		{
			List<PaymentType> paymentTypes = await paymentTypeRepository.GetAllAsync();

			if (paymentTypes.Count > 0)
			{
				return paymentTypes.Select(i =>
				new PaymentTypeVM
				{
					ID = i.ID,
					Name = i.Name,
					Fee = i.Fee
				}).ToList();
			}

			return null;
		}

		public async Task<OrderDetailVM> GetUserOrderDetailAsync(int orderID, int userID)
		{
			LoginAccount loginAccount = await loginAccountRepository.GetObjectAsync(userID);
			Order order = await orderRepository.GetObjectAsync(orderID);

			if (loginAccount is not null && order is not null)
			{
				if (order.DeliveryAddressID != null)
				{
					order.DeliveryAddress = await addressRepository.GetObjectAsync((int)order.DeliveryAddressID);
				}
				order.InvoicingAddress = await addressRepository.GetObjectAsync((int)order.InvoicingAddressID);
				await dataLoader.LoadAsync(order, i => i.DeliveryType);
				await dataLoader.LoadAsync(order, i => i.OrderState);
				await dataLoader.LoadAsync(order, i => i.PaymentState);
				await dataLoader.LoadAsync(order, i => i.PaymentType);
				await dataLoader.LoadAsync(order, i => i.OrderItems);
				await dataLoader.LoadAllAsync(order.OrderItems, i => i.Product);
				await dataLoader.LoadAllAsync(order.OrderItems, i => i.Product.ProductImages);

				return new OrderDetailVM()
				{
					ID = order.ID,
					DeliveryAddress = order.DeliveryAddress is not null
						? new Contracts.Customer.Addresses.AddressDto
						{
							FirstName = order.DeliveryAddress.FirstName,
							LastName = order.DeliveryAddress.LastName,
							PhoneNumber = order.DeliveryAddress.PhoneNumber,
							City = order.DeliveryAddress.City,
							Street = order.DeliveryAddress.Street,
							ZipCode = order.DeliveryAddress.ZipCode
						}
						: null,
					InvoicingAddress = new Contracts.Customer.Addresses.AddressDto
					{
						FirstName = order.InvoicingAddress.FirstName,
						LastName = order.InvoicingAddress.LastName,
						PhoneNumber = order.InvoicingAddress.PhoneNumber,
						City = order.InvoicingAddress.City,
						Street = order.InvoicingAddress.Street,
						ZipCode = order.InvoicingAddress.ZipCode
					},
					DeliveryType = order.DeliveryType.Name,
					OrderState = order.OrderState.Name,
					PaymentState = order.PaymentState.Name,
					PaymentType = order.PaymentType.Name,
					OrderItems = order.OrderItems.Select(i => new OrderItemVM
					{
						ID = i.ID,
						ProductID = i.Product.ID,
						Name = i.Product.Name,
						Amount = i.Amount,
						Price = i.Price,
						TotalPrice = i.Price * i.Amount,
						ImageID = i.Product.ProductImages.FirstOrDefault()?.ID ?? 0 // je toto spravne?
					}).ToList(),
					OrderedDate = (DateTime)order.OrderedDate,
					TotalPrice = order.TotalPrice
				};
			}

			return null;
		}

		public async Task<List<ShoppingCartItemVM>> GetUserShoppingCartItemsAsync(int userID)
		{
			Order order = await orderRepository.GetOrderInCartByCustomerIDAsync(userID);

			if (order is not null)
			{
				await dataLoader.LoadAsync(order, i => i.OrderItems);
				await dataLoader.LoadAllAsync(order.OrderItems, i => i.Product);
				await dataLoader.LoadAllAsync(order.OrderItems, i => i.Product.ProductCategory);
				await dataLoader.LoadAllAsync(order.OrderItems, i => i.Product.ProductImages);

				return order.OrderItems.Select(i => new ShoppingCartItemVM
				{
					ID = i.ID,
					Amount = i.Amount,
					AvailableQuantity = i.Product.AvailableQuantity,
					Name = i.Product.Name,
					Price = (decimal)((i.Product.ReducedPrice != null && i.Product.ReducedPrice > 0 && i.Product.ReducedPrice < i.Product.Price)
								? i.Product.ReducedPrice
								: i.Product.Price),
					ProductCategoryID = i.Product.ProductCategoryID,
					ProductCategoryName = i.Product.ProductCategory?.Name ?? string.Empty,
					ProductID = i.Product.ID,
					ProductImageID = i.Product.ProductImages.FirstOrDefault()?.ID
				}).OrderBy(i => i.Name).ToList();
			}

			return new List<ShoppingCartItemVM>();
		}

		public async Task<ShoppingCartItemsAmountVM> GetUserShoppingCartItemsAmountAsync(int userID)
		{
			Order order = await orderRepository.GetOrderInCartByCustomerIDAsync(userID);
			await dataLoader.LoadAsync(order, i => i.OrderItems);

			if (order is not null)
			{
				return new ShoppingCartItemsAmountVM()
				{
					Amount = order.OrderItems.Count
				};
			}

			return null;
		}

		/// <summary>
		/// Decreases the amount of the order item in the order or delete order item from the orde whose amount is less than 1
		/// </summary>
		/// <param name="userID"></param>
		/// <param name="orderItemIM"></param>
		/// <returns>New amount of order items in the order</returns>
		public async Task<ShoppingCartItemsAmountVM> RemoveOrderItemAsync(int userID, OrderItemIM orderItemIM)
		{
			Order order = await orderRepository.GetOrderInCartByCustomerIDAsync(userID);
			if (order is not null)
			{
				await dataLoader.LoadAsync(order, i => i.OrderItems);
				await dataLoader.LoadAllAsync(order.OrderItems, i => i.Product);

				OrderItem orderItem = order.OrderItems.Find(i => i.ProductID == orderItemIM.ProductID);
				Product product = await productRepository.GetObjectAsync(orderItemIM.ProductID);
				if (orderItem is not null && product is not null)
				{
					//product.AvailableQuantity += orderItemIM.Amount;
					orderItem.Amount -= orderItemIM.Amount;

					if (orderItem.Amount <= 0)
					{
						unitOfWork.AddForDelete(orderItem);
						//unitOfWork.AddForUpdate(product);

						// pozor
						//return new ShoppingCartItemsAmountVM() { Amount = order.OrderItems.Count };
					}
					else
					{
						unitOfWork.AddForUpdate(orderItem);
						//unitOfWork.AddForUpdate(product);

						//return new ShoppingCartItemsAmountVM() { Amount = order.OrderItems.Count };
					}

					//unitOfWork.AddForUpdate(product);
					await unitOfWork.CommitAsync();
					return new ShoppingCartItemsAmountVM() { Amount = order.OrderItems.Count };
				}

				return null; // could not find the order item or product => do nothing and inform user that something went wrong
			}

			return null; // could not find the order => do nothing and inform user that something went wrong
		}

		private bool AreAddressesEquals(Address left, Address right)
		{
			if (Object.ReferenceEquals(left, null) || Object.ReferenceEquals(right, null))
			{
				return false;
			}

			if (Object.ReferenceEquals(left, right))
			{
				return true;
			}

			return left.FirstName == right.FirstName &&
				left.LastName == right.LastName &&
				left.PhoneNumber == right.PhoneNumber &&
				left.City == right.City &&
				left.Street == right.Street &&
				left.ZipCode == right.ZipCode;
		}
	}
}
