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

namespace DiplomovaPrace.Facades.Orders
{
	public class OrderFacade : IOrderFacadeContract
	{
		private readonly IAdminOrderPriceCalculator adminOrderPriceCalculator;
		private readonly IOrderListQuery orderListQuery;
		private readonly IOrderRepository orderRepository;
		private readonly IOrderItemRepository orderItemRepository;
		private readonly IProductRepository productRepository;
		private readonly IAddressRepository addressRepository;
		private readonly IDataLoader dataLoader;
		private readonly IUnitOfWork unitOfWork;
		private readonly IDeliveryTypeRepository deliveryTypeRepository;
		private readonly IOrderStateRepository orderStateRepository;
		private readonly IPaymentStateRepository paymentStateRepository;
		private readonly IPaymentTypeRepository paymentTypeRepository;
		private readonly IMailerService mailerService;

		public OrderFacade(
			IAdminOrderPriceCalculator adminOrderPriceCalculator,
			IOrderListQuery orderListQuery,
			IOrderRepository orderRepository,
			IOrderItemRepository orderItemRepository,
			IProductRepository productRepository,
			IAddressRepository addressRepository,
			IDataLoader dataLoader,
			IUnitOfWork unitOfWork,
			IDeliveryTypeRepository deliveryTypeRepository,
			IOrderStateRepository orderStateRepository,
			IPaymentStateRepository paymentStateRepository,
			IPaymentTypeRepository paymentTypeRepository,
			IMailerService mailerService)
		{
			this.adminOrderPriceCalculator = adminOrderPriceCalculator;
			this.orderListQuery = orderListQuery;
			this.orderRepository = orderRepository;
			this.orderItemRepository = orderItemRepository;
			this.productRepository = productRepository;
			this.addressRepository = addressRepository;
			this.dataLoader = dataLoader;
			this.unitOfWork = unitOfWork;
			this.deliveryTypeRepository = deliveryTypeRepository;
			this.orderStateRepository = orderStateRepository;
			this.paymentStateRepository = paymentStateRepository;
			this.paymentTypeRepository = paymentTypeRepository;
			this.mailerService = mailerService;
		}

		public async Task<PagedQueryResult<OrderListItem>> GetOrderListAsync(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<OrderIM> GetOrderToUpdateByIDAsync(ValueDto<int> orderID)
		{
			Order order = await orderRepository.GetObjectAsync(orderID.Value).ConfigureAwait(false);
			await dataLoader.LoadAsync(order, i => i.OrderItems).ConfigureAwait(false);
			await dataLoader.LoadAllAsync(order.OrderItems, i => i.Product).ConfigureAwait(false);
			await dataLoader.LoadAsync(order, i => i.LoginAccount).ConfigureAwait(false);

			if (order is not null && order.OrderedDate is not null)
			{
				OrderIM orderIM = new()
				{
					InvoicingAddressID = order.InvoicingAddressID.Value,
					DeliveryAddressID = order.DeliveryAddressID ?? -1,
					CustomerEmail = order.CustomerEmail,
					OrderItems = order.OrderItems.Select(i =>
					new OrderItemIM
					{
						ID = i.ID,
						ProductID = i.ProductID,
						Name = i.Product.Name,
						Amount = i.Amount,
						Price = i.Price,
					}).ToList(),
					DeliveryTypeID = order.DeliveryTypeID.Value,
					OrderStateID = order.OrderStateID,
					PaymentStateID = order.PaymentStateID.Value,
					PaymentTypeID = order.PaymentTypeID.Value,
					OrderedDate = order.OrderedDate.Value
				};

				if (order.LoginAccount is not null)
				{
					orderIM.LoginAccount = new LoginAccountVM
					{
						FirstName = order.LoginAccount.FirstName,
						LastName = order.LoginAccount.LastName,
						Email = order.LoginAccount.Email
					};
				}

				return orderIM;
			}

			return new OrderIM();
		}

		public async Task<OrderDetailVM> GetOrderDetailByIDAsync(ValueDto<int> orderID)
		{
			Order order = await orderRepository.GetObjectAsync(orderID.Value).ConfigureAwait(false);
			await dataLoader.LoadAsync(order, i => i.OrderItems).ConfigureAwait(false);
			await dataLoader.LoadAllAsync(order.OrderItems, i => i.Product).ConfigureAwait(false);
			await dataLoader.LoadAsync(order, i => i.DeliveryAddress).ConfigureAwait(false);
			await dataLoader.LoadAsync(order, i => i.InvoicingAddress).ConfigureAwait(false);
			await dataLoader.LoadAsync(order, i => i.LoginAccount).ConfigureAwait(false);

			if (order is not null && order.OrderedDate is not null)
			{
				OrderDetailVM orderDetail = new()
				{
					DeliveryAddress = order.DeliveryAddress is null
					? null
					: new AddressVM
					{
						FirstName = order.DeliveryAddress.FirstName,
						LastName = order.DeliveryAddress.LastName,
						PhoneNumber = order.DeliveryAddress.PhoneNumber,
						City = order.DeliveryAddress.City,
						Street = order.DeliveryAddress.Street,
						ZipCode = order.DeliveryAddress.ZipCode,
						State = order.DeliveryAddress.State
					},
					InvoicingAddress = new AddressVM
					{
						FirstName = order.InvoicingAddress.FirstName,
						LastName = order.InvoicingAddress.LastName,
						PhoneNumber = order.InvoicingAddress.PhoneNumber,
						City = order.InvoicingAddress.City,
						Street = order.InvoicingAddress.Street,
						ZipCode = order.InvoicingAddress.ZipCode,
						State = order.InvoicingAddress.State
					},
					CustomerEmail = order.CustomerEmail,
					OrderItems = order.OrderItems.Select(i =>
					new OrderItemVM
					{
						ID = i.ID,
						ProductID = i.ProductID,
						Name = i.Product.Name,
						Amount = i.Amount,
						Price = i.Price,
						TotalPrice = i.Amount * i.Price
					}).ToList(),
					TotalPrice = order.TotalPrice,
					DeliveryTypeID = order.DeliveryTypeID,
					OrderStateID = order.OrderStateID,
					PaymentStateID = order.PaymentStateID,
					PaymentTypeID = order.PaymentTypeID,
					OrderedDate = order.OrderedDate
				};

				if (order.LoginAccount is not null)
				{
					orderDetail.LoginAccount = new()
					{
						FirstName = order.LoginAccount.FirstName,
						LastName = order.LoginAccount.LastName,
						Email = order.LoginAccount.Email
					};
				}

				return orderDetail;
			}

			return new OrderDetailVM();
		}

		public async Task UpdateOrderAsync(OrderIM inputModel)
		{
			// řešit počítání položek na skladě
			Order order = await orderRepository.GetObjectAsync(inputModel.OrderID).ConfigureAwait(false);

			if (order is null)
			{
				return;
			}

			// first load order items
			await dataLoader.LoadAsync(order, i => i.OrderItems).ConfigureAwait(false);
			// then load products to all order items
			await dataLoader.LoadAllAsync(order.OrderItems, i => i.Product).ConfigureAwait(false);
			await dataLoader.LoadAsync(order, i => i.LoginAccount);
			await dataLoader.LoadAsync(order, i => i.DeliveryType);
			await dataLoader.LoadAsync(order, i => i.PaymentType);

			// update invoicing address
			if (order.InvoicingAddressID != inputModel.InvoicingAddressID)
			{
				Address invoicingAddress = await addressRepository.GetObjectAsync(inputModel.InvoicingAddressID).ConfigureAwait(false);

				if (invoicingAddress is null)
				{
					return;
				}

				order.InvoicingAddressID = invoicingAddress.ID;
			}

			// update delivery address
			if (inputModel.DeliveryAddressID == -1)
			{
				if (order.LoginAccount is not null)
				{
					order.DeliveryAddressID = null;
				}
				else // delete delivery address withou login account
				{
					if (inputModel.DeliveryAddress is not null)
					{
						Address deliveryAddress = await addressRepository.GetObjectAsync(inputModel.DeliveryAddress.ID);
						unitOfWork.AddForDelete(deliveryAddress);
					}
				}
			}
			else if (order.DeliveryAddressID != inputModel.DeliveryAddressID)
			{
				Address deliveryAddress = await addressRepository.GetObjectAsync(inputModel.DeliveryAddressID);

				if (deliveryAddress is null)
				{
					return;
				}

				order.DeliveryAddressID = deliveryAddress.ID;
			}

			// items with default ID are added
			List<OrderItemIM> addedOrderItems = inputModel.OrderItems.Where(i => i.ID == default).ToList();
			foreach (var item in addedOrderItems)
			{
				Product product = await productRepository.GetObjectAsync(item.ProductID);
				if (product is not null)
				{
					// update the amount
					//product.AvailableQuantity -= item.Amount; // check negative value?

					OrderItem orderItem = new()
					{
						Amount = item.Amount,
						Price = item.Price, // admin can (could) change the price of a product
						ProductID = item.ProductID,
						OrderID = order.ID
					};

					//unitOfWork.AddForUpdate(product);
					unitOfWork.AddForInsert(orderItem);
				}
			}

			// items, which are in original collection, but not in inputModel collection, are items to be removed
			List<OrderItem> removedOrderItems = order.OrderItems.Where(i => !inputModel.OrderItems.Any(j => j.ID == i.ID)).ToList();
			foreach (var item in removedOrderItems)
			{
				// Nejspíš nebudu řešit odečítání a přičítání produktů při editaci objednávky adminem
				// Admin si pak sám vleze na editaci produktu a změní si počet
				//Product product = await productRepository.GetObjectAsync(item.ProductID);
				//if (product is not null)
				//{
				//	product.AvailableQuantity += item.Amount;
				//	unitOfWork.AddForUpdate(product);
				//}

				unitOfWork.AddForDelete(item);
			}

			// rest of the order items, which need to be checked, if the amount or the price, has not changed
			List<OrderItemIM> restOfOrderItems = inputModel.OrderItems.Except(addedOrderItems).ToList();
			foreach (var item in restOfOrderItems)
			{
				Product product = await productRepository.GetObjectAsync(item.ProductID);
				OrderItem orderItem = order.OrderItems.Find(i => i.ID == item.ID); // always should be found

				if (product is not null && orderItem is not null)
				{
					int newAmount = orderItem.Amount - item.Amount;

					// Admin si sám upraví dostupné množství produktů po editaci objednávky
					//product.AvailableQuantity += newAmount; // check negative value
					orderItem.Amount = item.Amount;
					orderItem.Price = item.Price;

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

			// WRONG CODE
			//// check added order items
			//if (order.OrderItems.Count < inputModel.OrderItems.Count)
			//{
			//	foreach (var orderItem in inputModel.OrderItems)
			//	{
			//		if (!order.OrderItems.Any(i => i.ID == orderItem.ID))
			//		{
			//			Product product = await productRepository.GetObjectAsync(orderItem.ProductID).ConfigureAwait(false);

			//			if (product is not null)
			//			{
			//				OrderItem newOrderItem = new()
			//				{
			//					Amount = orderItem.Amount,
			//					Price = orderItem.Price,
			//					OrderID = order.ID,
			//					ProductID = orderItem.ProductID
			//				};

			//				product.AvailableQuantity -= orderItem.Amount; // check negative value

			//				unitOfWork.AddForUpdate(product);
			//				unitOfWork.AddForInsert(newOrderItem);
			//				await unitOfWork.CommitAsync().ConfigureAwait(false);
			//			}
			//		}
			//	}
			//}
			//else if (order.OrderItems.Count > inputModel.OrderItems.Count) // check removed order items
			//{
			//	foreach (var orderItem in order.OrderItems)
			//	{
			//		if (!inputModel.OrderItems.Any(i => i.ID == orderItem.ID))
			//		{
			//			// get product and add ammount of the removed order item
			//			Product product = await productRepository.GetObjectAsync(orderItem.ProductID).ConfigureAwait(false);
			//			product.AvailableQuantity += orderItem.Amount;

			//			// get order item to remove
			//			OrderItem orderitemToDelete = await orderItemRepository.GetObjectAsync(orderItem.ID).ConfigureAwait(false);

			//			unitOfWork.AddForUpdate(product);
			//			unitOfWork.AddForDelete(orderitemToDelete);
			//			await unitOfWork.CommitAsync().ConfigureAwait(false);
			//		}
			//	}
			//}

			//// check ammount and price of order items
			//foreach (var orderItem in order.OrderItems)
			//{
			//	foreach (var orderItemIM in inputModel.OrderItems)
			//	{
			//		if (orderItemIM.ID == orderItem.ID && (orderItemIM.Amount != orderItem.Amount))
			//		{
			//			OrderItem orderItemToUpdate = await orderItemRepository.GetObjectAsync(orderItem.ID).ConfigureAwait(false);
			//			Product product = await productRepository.GetObjectAsync(orderItemToUpdate.ProductID).ConfigureAwait(false);

			//			// calculate new amount of order item
			//			int amount = orderItem.Amount - orderItemIM.Amount;
			//			// update quantity of product
			//			// check negative value
			//			product.AvailableQuantity += amount;
			//			// set new amount to order item
			//			orderItemToUpdate.Amount = orderItemIM.Amount;
			//			orderItemToUpdate.Price = orderItemIM.Price;

			//			unitOfWork.AddForUpdate(orderItemToUpdate);
			//			unitOfWork.AddForUpdate(product);
			//			await unitOfWork.CommitAsync().ConfigureAwait(false);
			//		}
			//	}
			//}

			// Validate data
			order.DeliveryTypeID = (await deliveryTypeRepository.GetObjectAsync(inputModel.DeliveryTypeID))?.ID ?? order.DeliveryTypeID;
			order.OrderStateID = (await orderStateRepository.GetObjectAsync(inputModel.OrderStateID))?.ID ?? order.OrderStateID;
			order.PaymentStateID = (await paymentStateRepository.GetObjectAsync(inputModel.PaymentStateID))?.ID ?? order.PaymentStateID;
			order.PaymentTypeID = (await paymentTypeRepository.GetObjectAsync(inputModel.PaymentTypeID))?.ID ?? order.PaymentTypeID;

			// Inform customer that his order is ready for pick up
			if (Math.Abs((int)OrderState.Entry.ReadyForPickUp) == order.OrderStateID)
			{
				string email = string.IsNullOrEmpty(order?.LoginAccount?.Email) ? order.CustomerEmail : order.LoginAccount.Email;
				string fullName = order.LoginAccount is not null ? $"{order.LoginAccount.FirstName} {order.LoginAccount.LastName}" : "";

				await mailerService.SendOrderRdyForPickEmailAsync(email, fullName, order.ID);
			}

			// calculate total price of the order
			List<OrderItem> orderItems = await orderItemRepository.GetOrderItemsByOrderIDAsync(order.ID).ConfigureAwait(false);
			order.TotalPrice = adminOrderPriceCalculator.CalculateTotalOrderPrice(orderItems, order.DeliveryType, order.PaymentType);

			unitOfWork.AddForUpdate(order);
			await unitOfWork.CommitAsync().ConfigureAwait(false);
		}

		public async Task<ValueDto<bool>> DeleteOrderAsync(ValueDto<int> orderId)
		{
			Contract.Requires<ArgumentNullException>(orderId is not null, nameof(orderId));
			Order order = await orderRepository.GetObjectAsync(orderId.Value).ConfigureAwait(false);

			if (order == null)
			{
				return new ValueDto<bool> { Value = false };
			}

			unitOfWork.AddForDelete(order);
			await unitOfWork.CommitAsync().ConfigureAwait(false);

			return new ValueDto<bool> { Value = true };
		}
	}
}
