using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using DiplomovaPrace.Contracts;
using DiplomovaPrace.Contracts.Products;
using DiplomovaPrace.Contracts.Products.InputModels;
using DiplomovaPrace.Contracts.Products.ViewModels;
using DiplomovaPrace.DataLayer.Queries.Products;
using DiplomovaPrace.DataLayer.Repositories.Products;
using DiplomovaPrace.Facades.Infrastructure;
using DiplomovaPrace.Model.Products;
using Havit.Data.Patterns.DataLoaders;
using Havit.Data.Patterns.UnitOfWorks;
using Havit.Diagnostics.Contracts;
using Havit.Extensions.DependencyInjection.Abstractions;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Options;

namespace DiplomovaPrace.Facades.Products
{
	[Service]
	public class ProductFacade : IProductFacadeContract, IProductFacade
	{
		private readonly IUnitOfWork unitOfWork;
		private readonly IDataLoader dataLoader;
		private readonly IProductRepository productRepository;
		private readonly IProductImageRepository productImageRepository;
		private readonly IProductListQuery productListQuery;
		private readonly IProductCategoryRepository productCategoryRepository;
		private readonly IWebHostEnvironment webHostEnvironment;
		private readonly ProductImageStorage productImageStorage;

		public ProductFacade(
			IUnitOfWork unitOfWork,
			IDataLoader dataLoader,
			IProductRepository productRepository,
			IProductImageRepository productImageRepository,
			IProductListQuery productListQuery,
			IProductCategoryRepository productCategoryRepository,
			IWebHostEnvironment webHostEnvironment,
			IOptions<ProductImageStorage> productImageStorage)
		{
			this.unitOfWork = unitOfWork;
			this.dataLoader = dataLoader;
			this.productRepository = productRepository;
			this.productImageRepository = productImageRepository;
			this.productListQuery = productListQuery;
			this.productCategoryRepository = productCategoryRepository;
			this.webHostEnvironment = webHostEnvironment;
			this.productImageStorage = productImageStorage.Value;
		}

		private static string GetRootPath()
		{
			//return webHostEnvironment.ContentRootPath;
			return Directory.GetCurrentDirectory();
		}

		private string GetStoragePath(string folderName)
		{
			return $"{productImageStorage.ProductImagePath}{folderName}\\";
		}

		// TODO: Bud nějak sjednotit jednotlivé modely, nebo kompletně oddělit queries od sebe (admin a customer budou mít každý svou vlastní třídu a modely)
		public async Task<PagedQueryResult<ProductDto>> GetProductListAsync(PagedQueryRequest<ProductListQueryFilter> productListRequest)
		{
			Contract.Requires<ArgumentNullException>(productListRequest is not null, nameof(productListRequest));

			productListQuery.Filter = new() {
				ProductCategoryID = productListRequest.Filter.ProductCategoryID,
				SearchString = productListRequest.Filter.SearchString
			};
			productListQuery.Sorting = productListRequest.Sorting;

			var result = await productListQuery.GetPageAsync(productListRequest.PageIndex, productListRequest.PageSize);

			return new PagedQueryResult<ProductDto>()
			{
				Data = result.Data.Select(i => new ProductDto
				{
					ID = i.ID,
					Name = i.Name,
					Price = i.Price,
					AvailableQuantity = i.AvailableQuantity,
					ReducedPrice = i.ReducedPrice,
					Description = i.Description,
					ProductCategoryID = i.ProductCategoryID,
					ProductImageIDs = i.ProductImageIDs
				}).ToList(),
				TotalCount = result.TotalCount
			};
		}

		public async Task<ProductDto> CreateProductAsync(ProductIM inputModel)
		{
			ProductCategory productCategory = null;

			if (inputModel.CategoryID is not null)
			{
				productCategory = await productCategoryRepository.GetObjectAsync((int)inputModel.CategoryID).ConfigureAwait(false);
			}

			Product product = new()
			{
				Name = inputModel.Name,
				Price = inputModel.Price,
				AvailableQuantity = inputModel.AvailableQuantity,
				Description = inputModel.Description,
				ProductCategoryID = productCategory is not null ? productCategory.ID : null
			};

			unitOfWork.AddForInsert(product);
			await unitOfWork.CommitAsync().ConfigureAwait(false);

			if ((inputModel.Image is not null) && (inputModel.Image.Length > 0))
			{
				await UploadProductImageAsync(new ProductImageIM
				{
					ProductID = product.ID,
					FileName = inputModel.FileName,
					Image = inputModel.Image
				}).ConfigureAwait(false);
			}

			return new ProductDto
			{
				ID = product.ID,
				Name = product.Name,
				Price = product.Price,
				AvailableQuantity = product.AvailableQuantity,
				Description = product.Description,
				ProductCategoryID = product.ProductCategoryID,
				ProductImageIDs = product.ProductImages.Select(i => i.ID).ToList()
			};
		}

		public async Task<ProductDto> GetProductAsync(GetProductRequest request)
		{
			Product product = await productRepository.GetObjectAsync(request.ID).ConfigureAwait(false);
			await dataLoader.LoadAsync(product, i => i.ProductImages).ConfigureAwait(false);

			if (product is not null)
			{
				return new ProductDto
				{
					ID = product.ID,
					Name = product.Name,
					Price = product.Price,
					ReducedPrice = product.ReducedPrice,
					AvailableQuantity = product.AvailableQuantity,
					Description = product.Description,
					ProductCategoryID = product.ProductCategoryID,
					ProductImageIDs = product.ProductImages.Select(i => i.ID).ToList()
				};
			}

			return new ProductDto();
		}

		public async Task<List<ProductDto>> GetAllProductsAsync()
		{
			List<Product> products = await productRepository.GetAllAsync().ConfigureAwait(false);

			if (products.Count > 0)
			{
				return products.OrderBy(i => i.Name)
					.Select(i => new ProductDto
					{
						ID = i.ID,
						Name = i.Name,
						Price = i.Price,
						ReducedPrice = i.ReducedPrice,
						AvailableQuantity = i.AvailableQuantity,
						Description = i.Description,
						ProductCategoryID = i.ProductCategoryID
					}).ToList();
			}

			return new List<ProductDto>();
		}

		public async Task UpdateProductAsync(ProductDto inputModel)
		{
			Product product = await productRepository.GetObjectAsync(inputModel.ID).ConfigureAwait(false);

			if (product is not null)
			{
				ProductCategory productCategory = null;

				if (inputModel.ProductCategoryID is not null)
				{
					productCategory = await productCategoryRepository.GetObjectAsync((int)inputModel.ProductCategoryID).ConfigureAwait(false);
				}

				product.Name = inputModel.Name;
				product.Price = inputModel.Price;
				product.ReducedPrice = inputModel.ReducedPrice;
				product.AvailableQuantity = inputModel.AvailableQuantity;
				product.ProductCategoryID = productCategory is not null ? productCategory.ID : null;
				product.Description = inputModel.Description;

				foreach (var image in inputModel.ProductImages)
				{
					await UploadProductImageAsync(image).ConfigureAwait(false);
				}

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

		public async Task<DeleteProductResult> DeleteProductAsync(DeleteProductRequest request)
		{
			Product product = await productRepository.GetObjectAsync(request.ID).ConfigureAwait(false);
			await dataLoader.LoadAsync(product, i => i.ProductImages).ConfigureAwait(false);

			if (product is not null)
			{
				foreach (ProductImage productImage in product.ProductImages)
				{
					string fullPath = GetRootPath() + GetStoragePath(productImage.StorageName.Substring(0, 2)) + productImage.StorageName;

					if (File.Exists(fullPath))
					{
						File.Delete(fullPath);
					}

					unitOfWork.AddForDelete(productImage);
				}

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

				return new DeleteProductResult { IsSuccessful = true };
			}

			return new DeleteProductResult { IsSuccessful = false };
		}

		public async Task<FileStream> GetProductImageAsync(int productImageID)
		{
			ProductImage image = await productImageRepository.GetObjectAsync(productImageID).ConfigureAwait(false);

			if (image is not null)
			{
				string storagePath = GetRootPath() + GetStoragePath(image.StorageName.Substring(0, 2));

				return File.OpenRead(storagePath + image.StorageName);
			}

			return null;
		}

		// TODO: melo by se kontrolovat productID v inputModel
		public async Task<ProductImageVM> UploadProductImageAsync(ProductImageIM inputModel)
		{
			if ((inputModel.Image is null) || (inputModel.Image.Length == 0))
			{
				return new ProductImageVM();
			}

			string extension = Path.GetExtension(inputModel.FileName);

			string storageName = $"{Guid.NewGuid()}{extension}";
			string folderName = storageName.Substring(0, 2);
			string storagePath = GetStoragePath(folderName);
			string filePath = storagePath + storageName;
			string completePath = GetRootPath() + filePath;
			Directory.CreateDirectory(Path.GetDirectoryName(completePath));

			using (FileStream fileStream = new(completePath, FileMode.Create))
			{
				using MemoryStream memoryStream = new(inputModel.Image);
				await memoryStream.CopyToAsync(fileStream).ConfigureAwait(false);
			}

			ProductImage productImage = new()
			{
				ProductID = inputModel.ProductID,
				StorageName = storageName
			};

			unitOfWork.AddForInsert(productImage);
			await unitOfWork.CommitAsync().ConfigureAwait(false);

			return new ProductImageVM { ID = productImage.ID };
		}

		public async Task<DeleteProductImageResult> DeleteProductImageAsync(DeleteProductImageRequest request)
		{
			ProductImage productImage = await productImageRepository.GetObjectAsync(request.ID).ConfigureAwait(false);

			if (productImage is not null)
			{
				//string directoryPath = Path.Combine(GetRootPath(), GetStoragePath(productImage.StorageName.Substring(0, 2)));
				//string fullPath = Path.Combine(directoryPath, productImage.StorageName);
				string directoryPath = $"{GetRootPath()}{GetStoragePath(productImage.StorageName.Substring(0, 2))}";

				if (Directory.Exists(directoryPath))
				{
					Directory.Delete(directoryPath, true);
				}

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

				return new DeleteProductImageResult { IsSuccessful = true };
			}

			return new DeleteProductImageResult { IsSuccessful = false };
		}
	}
}